Mercurial > hg4j
comparison src/org/tmatesoft/hg/repo/HgRemoteRepository.java @ 687:9859fcea475d
Towards ssh remote repositories: refactor HgRemoteRepository - move http related code to HttpConnector
| author | Artem Tikhomirov <tikhomirov.artem@gmail.com> |
|---|---|
| date | Sat, 27 Jul 2013 18:34:14 +0200 |
| parents | 9897cbfd2790 |
| children | 24f4efedc9d5 |
comparison
equal
deleted
inserted
replaced
| 686:f1f095e42555 | 687:9859fcea475d |
|---|---|
| 14 * the terms of a license other than GNU General Public License | 14 * the terms of a license other than GNU General Public License |
| 15 * contact TMate Software at support@hg4j.com | 15 * contact TMate Software at support@hg4j.com |
| 16 */ | 16 */ |
| 17 package org.tmatesoft.hg.repo; | 17 package org.tmatesoft.hg.repo; |
| 18 | 18 |
| 19 import static org.tmatesoft.hg.util.LogFacility.Severity.Info; | 19 import static org.tmatesoft.hg.internal.remote.Connector.*; |
| 20 import static org.tmatesoft.hg.util.Outcome.Kind.Failure; | 20 import static org.tmatesoft.hg.util.Outcome.Kind.Failure; |
| 21 import static org.tmatesoft.hg.util.Outcome.Kind.Success; | 21 import static org.tmatesoft.hg.util.Outcome.Kind.Success; |
| 22 | 22 |
| 23 import java.io.BufferedReader; | 23 import java.io.BufferedReader; |
| 24 import java.io.ByteArrayOutputStream; | 24 import java.io.ByteArrayOutputStream; |
| 29 import java.io.InputStreamReader; | 29 import java.io.InputStreamReader; |
| 30 import java.io.OutputStream; | 30 import java.io.OutputStream; |
| 31 import java.io.StreamTokenizer; | 31 import java.io.StreamTokenizer; |
| 32 import java.net.ContentHandler; | 32 import java.net.ContentHandler; |
| 33 import java.net.ContentHandlerFactory; | 33 import java.net.ContentHandlerFactory; |
| 34 import java.net.HttpURLConnection; | |
| 35 import java.net.MalformedURLException; | |
| 36 import java.net.URL; | 34 import java.net.URL; |
| 37 import java.net.URLConnection; | 35 import java.net.URLConnection; |
| 38 import java.security.cert.CertificateException; | |
| 39 import java.security.cert.X509Certificate; | |
| 40 import java.util.ArrayList; | 36 import java.util.ArrayList; |
| 41 import java.util.Arrays; | 37 import java.util.Arrays; |
| 42 import java.util.Collection; | 38 import java.util.Collection; |
| 43 import java.util.Collections; | 39 import java.util.Collections; |
| 44 import java.util.HashSet; | 40 import java.util.HashSet; |
| 46 import java.util.LinkedHashMap; | 42 import java.util.LinkedHashMap; |
| 47 import java.util.LinkedList; | 43 import java.util.LinkedList; |
| 48 import java.util.List; | 44 import java.util.List; |
| 49 import java.util.Map; | 45 import java.util.Map; |
| 50 import java.util.Set; | 46 import java.util.Set; |
| 51 import java.util.prefs.BackingStoreException; | |
| 52 import java.util.prefs.Preferences; | |
| 53 import java.util.zip.InflaterInputStream; | 47 import java.util.zip.InflaterInputStream; |
| 54 | |
| 55 import javax.net.ssl.HttpsURLConnection; | |
| 56 import javax.net.ssl.SSLContext; | |
| 57 import javax.net.ssl.TrustManager; | |
| 58 import javax.net.ssl.X509TrustManager; | |
| 59 | 48 |
| 60 import org.tmatesoft.hg.core.HgBadArgumentException; | 49 import org.tmatesoft.hg.core.HgBadArgumentException; |
| 61 import org.tmatesoft.hg.core.HgIOException; | 50 import org.tmatesoft.hg.core.HgIOException; |
| 62 import org.tmatesoft.hg.core.HgRemoteConnectionException; | 51 import org.tmatesoft.hg.core.HgRemoteConnectionException; |
| 63 import org.tmatesoft.hg.core.HgRepositoryNotFoundException; | 52 import org.tmatesoft.hg.core.HgRepositoryNotFoundException; |
| 64 import org.tmatesoft.hg.core.Nodeid; | 53 import org.tmatesoft.hg.core.Nodeid; |
| 65 import org.tmatesoft.hg.core.SessionContext; | 54 import org.tmatesoft.hg.core.SessionContext; |
| 55 import org.tmatesoft.hg.internal.BundleSerializer; | |
| 66 import org.tmatesoft.hg.internal.DataSerializer; | 56 import org.tmatesoft.hg.internal.DataSerializer; |
| 67 import org.tmatesoft.hg.internal.DataSerializer.OutputStreamSerializer; | 57 import org.tmatesoft.hg.internal.DataSerializer.OutputStreamSerializer; |
| 68 import org.tmatesoft.hg.internal.BundleSerializer; | |
| 69 import org.tmatesoft.hg.internal.EncodingHelper; | 58 import org.tmatesoft.hg.internal.EncodingHelper; |
| 59 import org.tmatesoft.hg.internal.FileUtils; | |
| 70 import org.tmatesoft.hg.internal.Internals; | 60 import org.tmatesoft.hg.internal.Internals; |
| 71 import org.tmatesoft.hg.internal.PropertyMarshal; | 61 import org.tmatesoft.hg.internal.PropertyMarshal; |
| 62 import org.tmatesoft.hg.internal.remote.Connector; | |
| 63 import org.tmatesoft.hg.internal.remote.HttpConnector; | |
| 72 import org.tmatesoft.hg.util.LogFacility.Severity; | 64 import org.tmatesoft.hg.util.LogFacility.Severity; |
| 73 import org.tmatesoft.hg.util.Outcome; | 65 import org.tmatesoft.hg.util.Outcome; |
| 74 import org.tmatesoft.hg.util.Pair; | 66 import org.tmatesoft.hg.util.Pair; |
| 75 | 67 |
| 76 /** | 68 /** |
| 82 * @author Artem Tikhomirov | 74 * @author Artem Tikhomirov |
| 83 * @author TMate Software Ltd. | 75 * @author TMate Software Ltd. |
| 84 */ | 76 */ |
| 85 public class HgRemoteRepository implements SessionContext.Source { | 77 public class HgRemoteRepository implements SessionContext.Source { |
| 86 | 78 |
| 87 private final URL url; | |
| 88 private final SSLContext sslContext; | |
| 89 private final String authInfo; | |
| 90 private final boolean debug; | 79 private final boolean debug; |
| 91 private HgLookup lookupHelper; | 80 private HgLookup lookupHelper; |
| 92 private final SessionContext sessionContext; | 81 private final SessionContext sessionContext; |
| 93 private Set<String> remoteCapabilities; | 82 private Set<String> remoteCapabilities; |
| 83 private Connector remote; | |
| 94 | 84 |
| 95 static { | 85 static { |
| 96 URLConnection.setContentHandlerFactory(new ContentHandlerFactory() { | 86 URLConnection.setContentHandlerFactory(new ContentHandlerFactory() { |
| 97 | 87 |
| 98 public ContentHandler createContentHandler(String mimetype) { | 88 public ContentHandler createContentHandler(String mimetype) { |
| 121 | 111 |
| 122 HgRemoteRepository(SessionContext ctx, URL url) throws HgBadArgumentException { | 112 HgRemoteRepository(SessionContext ctx, URL url) throws HgBadArgumentException { |
| 123 if (url == null || ctx == null) { | 113 if (url == null || ctx == null) { |
| 124 throw new IllegalArgumentException(); | 114 throw new IllegalArgumentException(); |
| 125 } | 115 } |
| 126 this.url = url; | |
| 127 sessionContext = ctx; | 116 sessionContext = ctx; |
| 128 debug = new PropertyMarshal(ctx).getBoolean("hg4j.remote.debug", false); | 117 debug = new PropertyMarshal(ctx).getBoolean("hg4j.remote.debug", false); |
| 129 if ("https".equals(url.getProtocol())) { | 118 remote = new HttpConnector(); |
| 130 try { | 119 remote.init(url, ctx, null); |
| 131 sslContext = SSLContext.getInstance("SSL"); | |
| 132 class TrustEveryone implements X509TrustManager { | |
| 133 public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { | |
| 134 if (debug) { | |
| 135 System.out.println("checkClientTrusted:" + authType); | |
| 136 } | |
| 137 } | |
| 138 public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { | |
| 139 if (debug) { | |
| 140 System.out.println("checkServerTrusted:" + authType); | |
| 141 } | |
| 142 } | |
| 143 public X509Certificate[] getAcceptedIssuers() { | |
| 144 return new X509Certificate[0]; | |
| 145 } | |
| 146 }; | |
| 147 sslContext.init(null, new TrustManager[] { new TrustEveryone() }, null); | |
| 148 } catch (Exception ex) { | |
| 149 throw new HgBadArgumentException("Can't initialize secure connection", ex); | |
| 150 } | |
| 151 } else { | |
| 152 sslContext = null; | |
| 153 } | |
| 154 if (url.getUserInfo() != null) { | |
| 155 String ai = null; | |
| 156 try { | |
| 157 // Hack to get Base64-encoded credentials | |
| 158 Preferences tempNode = Preferences.userRoot().node("xxx"); | |
| 159 tempNode.putByteArray("xxx", url.getUserInfo().getBytes()); | |
| 160 ai = tempNode.get("xxx", null); | |
| 161 tempNode.removeNode(); | |
| 162 } catch (BackingStoreException ex) { | |
| 163 sessionContext.getLog().dump(getClass(), Info, ex, null); | |
| 164 // IGNORE | |
| 165 } | |
| 166 authInfo = ai; | |
| 167 } else { | |
| 168 authInfo = null; | |
| 169 } | |
| 170 } | 120 } |
| 171 | 121 |
| 172 public boolean isInvalid() throws HgRemoteConnectionException { | 122 public boolean isInvalid() throws HgRemoteConnectionException { |
| 173 initCapabilities(); | 123 initCapabilities(); |
| 174 return remoteCapabilities.isEmpty(); | 124 return remoteCapabilities.isEmpty(); |
| 176 | 126 |
| 177 /** | 127 /** |
| 178 * @return human-readable address of the server, without user credentials or any other security information | 128 * @return human-readable address of the server, without user credentials or any other security information |
| 179 */ | 129 */ |
| 180 public String getLocation() { | 130 public String getLocation() { |
| 181 if (url.getUserInfo() == null) { | 131 return remote.getServerLocation(); |
| 182 return url.toExternalForm(); | |
| 183 } | |
| 184 if (url.getPort() != -1) { | |
| 185 return String.format("%s://%s:%d%s", url.getProtocol(), url.getHost(), url.getPort(), url.getPath()); | |
| 186 } else { | |
| 187 return String.format("%s://%s%s", url.getProtocol(), url.getHost(), url.getPath()); | |
| 188 } | |
| 189 } | 132 } |
| 190 | 133 |
| 191 public SessionContext getSessionContext() { | 134 public SessionContext getSessionContext() { |
| 192 return sessionContext; | 135 return sessionContext; |
| 193 } | 136 } |
| 194 | 137 |
| 195 public List<Nodeid> heads() throws HgRemoteConnectionException { | 138 public List<Nodeid> heads() throws HgRemoteConnectionException { |
| 196 HttpURLConnection c = null; | 139 if (isInvalid()) { |
| 197 try { | 140 return Collections.emptyList(); |
| 198 URL u = new URL(url, url.getPath() + "?cmd=heads"); | 141 } |
| 199 c = setupConnection(u.openConnection()); | 142 try { |
| 200 c.connect(); | 143 remote.sessionBegin(); |
| 201 if (debug) { | 144 InputStreamReader is = new InputStreamReader(remote.heads(), "US-ASCII"); |
| 202 dumpResponseHeader(u, c); | |
| 203 } | |
| 204 InputStreamReader is = new InputStreamReader(c.getInputStream(), "US-ASCII"); | |
| 205 StreamTokenizer st = new StreamTokenizer(is); | 145 StreamTokenizer st = new StreamTokenizer(is); |
| 206 st.ordinaryChars('0', '9'); // wordChars performs |, hence need to 0 first | 146 st.ordinaryChars('0', '9'); // wordChars performs |, hence need to 0 first |
| 207 st.wordChars('0', '9'); | 147 st.wordChars('0', '9'); |
| 208 st.eolIsSignificant(false); | 148 st.eolIsSignificant(false); |
| 209 LinkedList<Nodeid> parseResult = new LinkedList<Nodeid>(); | 149 LinkedList<Nodeid> parseResult = new LinkedList<Nodeid>(); |
| 210 while (st.nextToken() != StreamTokenizer.TT_EOF) { | 150 while (st.nextToken() != StreamTokenizer.TT_EOF) { |
| 211 parseResult.add(Nodeid.fromAscii(st.sval)); | 151 parseResult.add(Nodeid.fromAscii(st.sval)); |
| 212 } | 152 } |
| 213 return parseResult; | 153 return parseResult; |
| 214 } catch (MalformedURLException ex) { | 154 } catch (IOException ex) { |
| 215 throw new HgRemoteConnectionException("Bad URL", ex).setRemoteCommand("heads").setServerInfo(getLocation()); | 155 throw new HgRemoteConnectionException("Communication failure", ex).setRemoteCommand(CMD_HEADS).setServerInfo(getLocation()); |
| 216 } catch (IOException ex) { | 156 } finally { |
| 217 throw new HgRemoteConnectionException("Communication failure", ex).setRemoteCommand("heads").setServerInfo(getLocation()); | 157 remote.sessionEnd(); |
| 218 } finally { | |
| 219 if (c != null) { | |
| 220 c.disconnect(); | |
| 221 } | |
| 222 } | 158 } |
| 223 } | 159 } |
| 224 | 160 |
| 225 public List<Nodeid> between(Nodeid tip, Nodeid base) throws HgRemoteConnectionException { | 161 public List<Nodeid> between(Nodeid tip, Nodeid base) throws HgRemoteConnectionException { |
| 226 Range r = new Range(base, tip); | 162 Range r = new Range(base, tip); |
| 232 * @param ranges | 168 * @param ranges |
| 233 * @return map, where keys are input instances, values are corresponding server reply | 169 * @return map, where keys are input instances, values are corresponding server reply |
| 234 * @throws HgRemoteConnectionException | 170 * @throws HgRemoteConnectionException |
| 235 */ | 171 */ |
| 236 public Map<Range, List<Nodeid>> between(Collection<Range> ranges) throws HgRemoteConnectionException { | 172 public Map<Range, List<Nodeid>> between(Collection<Range> ranges) throws HgRemoteConnectionException { |
| 237 if (ranges.isEmpty()) { | 173 if (ranges.isEmpty() || isInvalid()) { |
| 238 return Collections.emptyMap(); | 174 return Collections.emptyMap(); |
| 239 } | 175 } |
| 240 // if fact, shall do other way round, this method shall send | |
| 241 LinkedHashMap<Range, List<Nodeid>> rv = new LinkedHashMap<HgRemoteRepository.Range, List<Nodeid>>(ranges.size() * 4 / 3); | 176 LinkedHashMap<Range, List<Nodeid>> rv = new LinkedHashMap<HgRemoteRepository.Range, List<Nodeid>>(ranges.size() * 4 / 3); |
| 242 StringBuilder sb = new StringBuilder(20 + ranges.size() * 82); | 177 try { |
| 243 sb.append("pairs="); | 178 remote.sessionBegin(); |
| 244 for (Range r : ranges) { | 179 InputStreamReader is = new InputStreamReader(remote.between(ranges), "US-ASCII"); |
| 245 r.append(sb); | |
| 246 sb.append('+'); | |
| 247 } | |
| 248 if (sb.charAt(sb.length() - 1) == '+') { | |
| 249 // strip last space | |
| 250 sb.setLength(sb.length() - 1); | |
| 251 } | |
| 252 HttpURLConnection c = null; | |
| 253 try { | |
| 254 boolean usePOST = ranges.size() > 3; | |
| 255 URL u = new URL(url, url.getPath() + "?cmd=between" + (usePOST ? "" : '&' + sb.toString())); | |
| 256 c = setupConnection(u.openConnection()); | |
| 257 if (usePOST) { | |
| 258 c.setRequestMethod("POST"); | |
| 259 c.setRequestProperty("Content-Length", String.valueOf(sb.length()/*nodeids are ASCII, bytes == characters */)); | |
| 260 c.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); | |
| 261 c.setDoOutput(true); | |
| 262 c.connect(); | |
| 263 OutputStream os = c.getOutputStream(); | |
| 264 os.write(sb.toString().getBytes()); | |
| 265 os.flush(); | |
| 266 os.close(); | |
| 267 } else { | |
| 268 c.connect(); | |
| 269 } | |
| 270 if (debug) { | |
| 271 System.out.printf("%d ranges, method:%s \n", ranges.size(), c.getRequestMethod()); | |
| 272 dumpResponseHeader(u, c); | |
| 273 } | |
| 274 InputStreamReader is = new InputStreamReader(c.getInputStream(), "US-ASCII"); | |
| 275 StreamTokenizer st = new StreamTokenizer(is); | 180 StreamTokenizer st = new StreamTokenizer(is); |
| 276 st.ordinaryChars('0', '9'); | 181 st.ordinaryChars('0', '9'); |
| 277 st.wordChars('0', '9'); | 182 st.wordChars('0', '9'); |
| 278 st.eolIsSignificant(true); | 183 st.eolIsSignificant(true); |
| 279 Iterator<Range> rangeItr = ranges.iterator(); | 184 Iterator<Range> rangeItr = ranges.iterator(); |
| 313 currRangeList.addLast(nid); | 218 currRangeList.addLast(nid); |
| 314 } | 219 } |
| 315 } | 220 } |
| 316 is.close(); | 221 is.close(); |
| 317 return rv; | 222 return rv; |
| 318 } catch (MalformedURLException ex) { | 223 } catch (IOException ex) { |
| 319 throw new HgRemoteConnectionException("Bad URL", ex).setRemoteCommand("between").setServerInfo(getLocation()); | 224 throw new HgRemoteConnectionException("Communication failure", ex).setRemoteCommand(CMD_BETWEEN).setServerInfo(getLocation()); |
| 320 } catch (IOException ex) { | 225 } finally { |
| 321 throw new HgRemoteConnectionException("Communication failure", ex).setRemoteCommand("between").setServerInfo(getLocation()); | 226 remote.sessionEnd(); |
| 322 } finally { | |
| 323 if (c != null) { | |
| 324 c.disconnect(); | |
| 325 } | |
| 326 } | 227 } |
| 327 } | 228 } |
| 328 | 229 |
| 329 public List<RemoteBranch> branches(List<Nodeid> nodes) throws HgRemoteConnectionException { | 230 public List<RemoteBranch> branches(List<Nodeid> nodes) throws HgRemoteConnectionException { |
| 330 StringBuilder sb = appendNodeidListArgument("nodes", nodes, null); | 231 if (isInvalid()) { |
| 331 HttpURLConnection c = null; | 232 return Collections.emptyList(); |
| 332 try { | 233 } |
| 333 URL u = new URL(url, url.getPath() + "?cmd=branches&" + sb.toString()); | 234 try { |
| 334 c = setupConnection(u.openConnection()); | 235 remote.sessionBegin(); |
| 335 c.connect(); | 236 InputStreamReader is = new InputStreamReader(remote.branches(nodes), "US-ASCII"); |
| 336 if (debug) { | |
| 337 dumpResponseHeader(u, c); | |
| 338 } | |
| 339 InputStreamReader is = new InputStreamReader(c.getInputStream(), "US-ASCII"); | |
| 340 StreamTokenizer st = new StreamTokenizer(is); | 237 StreamTokenizer st = new StreamTokenizer(is); |
| 341 st.ordinaryChars('0', '9'); | 238 st.ordinaryChars('0', '9'); |
| 342 st.wordChars('0', '9'); | 239 st.wordChars('0', '9'); |
| 343 st.eolIsSignificant(false); | 240 st.eolIsSignificant(false); |
| 344 ArrayList<Nodeid> parseResult = new ArrayList<Nodeid>(nodes.size() * 4); | 241 ArrayList<Nodeid> parseResult = new ArrayList<Nodeid>(nodes.size() * 4); |
| 352 for (int i = 0; i < nodes.size(); i++) { | 249 for (int i = 0; i < nodes.size(); i++) { |
| 353 RemoteBranch rb = new RemoteBranch(parseResult.get(i*4), parseResult.get(i*4 + 1), parseResult.get(i*4 + 2), parseResult.get(i*4 + 3)); | 250 RemoteBranch rb = new RemoteBranch(parseResult.get(i*4), parseResult.get(i*4 + 1), parseResult.get(i*4 + 2), parseResult.get(i*4 + 3)); |
| 354 rv.add(rb); | 251 rv.add(rb); |
| 355 } | 252 } |
| 356 return rv; | 253 return rv; |
| 357 } catch (MalformedURLException ex) { | 254 } catch (IOException ex) { |
| 358 throw new HgRemoteConnectionException("Bad URL", ex).setRemoteCommand("branches").setServerInfo(getLocation()); | 255 throw new HgRemoteConnectionException("Communication failure", ex).setRemoteCommand(CMD_BRANCHES).setServerInfo(getLocation()); |
| 359 } catch (IOException ex) { | 256 } finally { |
| 360 throw new HgRemoteConnectionException("Communication failure", ex).setRemoteCommand("branches").setServerInfo(getLocation()); | 257 remote.sessionEnd(); |
| 361 } finally { | |
| 362 if (c != null) { | |
| 363 c.disconnect(); | |
| 364 } | |
| 365 } | 258 } |
| 366 } | 259 } |
| 367 | 260 |
| 368 /* | 261 /* |
| 369 * XXX need to describe behavior when roots arg is empty; our RepositoryComparator code currently returns empty lists when | 262 * XXX need to describe behavior when roots arg is empty; our RepositoryComparator code currently returns empty lists when |
| 380 * according to latter, bundleformat data is sent through zlib | 273 * according to latter, bundleformat data is sent through zlib |
| 381 * (there's no header like HG10?? with the server output, though, | 274 * (there's no header like HG10?? with the server output, though, |
| 382 * as one may expect according to http://mercurial.selenic.com/wiki/BundleFormat) | 275 * as one may expect according to http://mercurial.selenic.com/wiki/BundleFormat) |
| 383 */ | 276 */ |
| 384 public HgBundle getChanges(List<Nodeid> roots) throws HgRemoteConnectionException, HgRuntimeException { | 277 public HgBundle getChanges(List<Nodeid> roots) throws HgRemoteConnectionException, HgRuntimeException { |
| 278 if (isInvalid()) { | |
| 279 return null; // XXX valid retval??? | |
| 280 } | |
| 385 List<Nodeid> _roots = roots.isEmpty() ? Collections.singletonList(Nodeid.NULL) : roots; | 281 List<Nodeid> _roots = roots.isEmpty() ? Collections.singletonList(Nodeid.NULL) : roots; |
| 386 StringBuilder sb = appendNodeidListArgument("roots", _roots, null); | 282 try { |
| 387 HttpURLConnection c = null; | 283 remote.sessionBegin(); |
| 388 try { | 284 File tf = writeBundle(remote.changegroup(_roots), false, "HG10GZ" /*didn't see any other that zip*/); |
| 389 URL u = new URL(url, url.getPath() + "?cmd=changegroup&" + sb.toString()); | |
| 390 c = setupConnection(u.openConnection()); | |
| 391 c.connect(); | |
| 392 if (debug) { | 285 if (debug) { |
| 393 dumpResponseHeader(u, c); | 286 System.out.printf("Wrote bundle %s for roots %s\n", tf, roots); |
| 394 } | |
| 395 File tf = writeBundle(c.getInputStream(), false, "HG10GZ" /*didn't see any other that zip*/); | |
| 396 if (debug) { | |
| 397 System.out.printf("Wrote bundle %s for roots %s\n", tf, sb); | |
| 398 } | 287 } |
| 399 return getLookupHelper().loadBundle(tf); | 288 return getLookupHelper().loadBundle(tf); |
| 400 } catch (MalformedURLException ex) { // XXX in fact, this exception might be better to be re-thrown as RuntimeEx, | 289 } catch (IOException ex) { |
| 401 // as there's little user can do about this issue (URLs are constructed by our code) | 290 throw new HgRemoteConnectionException("Communication failure", ex).setRemoteCommand(CMD_CHANGEGROUP).setServerInfo(getLocation()); |
| 402 throw new HgRemoteConnectionException("Bad URL", ex).setRemoteCommand("changegroup").setServerInfo(getLocation()); | |
| 403 } catch (IOException ex) { | |
| 404 throw new HgRemoteConnectionException("Communication failure", ex).setRemoteCommand("changegroup").setServerInfo(getLocation()); | |
| 405 } catch (HgRepositoryNotFoundException ex) { | 291 } catch (HgRepositoryNotFoundException ex) { |
| 406 throw new HgRemoteConnectionException("Communication failure", ex).setRemoteCommand("changegroup").setServerInfo(getLocation()); | 292 throw new HgRemoteConnectionException("Communication failure", ex).setRemoteCommand(CMD_CHANGEGROUP).setServerInfo(getLocation()); |
| 407 } finally { | 293 } finally { |
| 408 if (c != null) { | 294 remote.sessionEnd(); |
| 409 c.disconnect(); | |
| 410 } | |
| 411 } | 295 } |
| 412 } | 296 } |
| 413 | 297 |
| 414 public void unbundle(HgBundle bundle, List<Nodeid> remoteHeads) throws HgRemoteConnectionException, HgRuntimeException { | 298 public void unbundle(HgBundle bundle, List<Nodeid> remoteHeads) throws HgRemoteConnectionException, HgRuntimeException { |
| 415 if (remoteHeads == null) { | 299 if (remoteHeads == null) { |
| 416 // TODO collect heads from bundle: | 300 // TODO collect heads from bundle: |
| 417 // bundle.inspectChangelog(new HeadCollector(for each c : if collected has c.p1 or c.p2, remove them. Add c)) | 301 // bundle.inspectChangelog(new HeadCollector(for each c : if collected has c.p1 or c.p2, remove them. Add c)) |
| 418 // or get from remote server??? | 302 // or get from remote server??? |
| 419 throw Internals.notImplemented(); | 303 throw Internals.notImplemented(); |
| 420 } | 304 } |
| 421 StringBuilder sb = appendNodeidListArgument("heads", remoteHeads, null); | 305 if (isInvalid()) { |
| 422 | 306 return; |
| 423 HttpURLConnection c = null; | 307 } |
| 424 DataSerializer.DataSource bundleData = BundleSerializer.newInstance(sessionContext, bundle); | 308 DataSerializer.DataSource bundleData = BundleSerializer.newInstance(sessionContext, bundle); |
| 425 try { | 309 OutputStream os = null; |
| 426 URL u = new URL(url, url.getPath() + "?cmd=unbundle&" + sb.toString()); | 310 try { |
| 427 c = setupConnection(u.openConnection()); | 311 remote.sessionBegin(); |
| 428 c.setRequestMethod("POST"); | 312 os = remote.unbundle(bundleData.serializeLength(), remoteHeads); |
| 429 c.setRequestProperty("Content-Length", String.valueOf(bundleData.serializeLength())); | |
| 430 c.setRequestProperty("Content-Type", "application/mercurial-0.1"); | |
| 431 c.setDoOutput(true); | |
| 432 c.connect(); | |
| 433 OutputStream os = c.getOutputStream(); | |
| 434 bundleData.serialize(new OutputStreamSerializer(os)); | 313 bundleData.serialize(new OutputStreamSerializer(os)); |
| 435 os.flush(); | 314 os.flush(); |
| 436 os.close(); | 315 os.close(); |
| 437 if (debug) { | 316 os = null; |
| 438 dumpResponseHeader(u, c); | |
| 439 dumpResponse(c); | |
| 440 } | |
| 441 checkResponseOk(c, "Push", "unbundle"); | |
| 442 } catch (MalformedURLException ex) { | |
| 443 throw new HgRemoteConnectionException("Bad URL", ex).setRemoteCommand("unbundle").setServerInfo(getLocation()); | |
| 444 } catch (IOException ex) { | 317 } catch (IOException ex) { |
| 445 throw new HgRemoteConnectionException("Communication failure", ex).setRemoteCommand("unbundle").setServerInfo(getLocation()); | 318 throw new HgRemoteConnectionException("Communication failure", ex).setRemoteCommand("unbundle").setServerInfo(getLocation()); |
| 446 } catch (HgIOException ex) { | 319 } catch (HgIOException ex) { |
| 447 throw new HgRemoteConnectionException("Communication failure", ex).setRemoteCommand("unbundle").setServerInfo(getLocation()); | 320 throw new HgRemoteConnectionException("Communication failure", ex).setRemoteCommand("unbundle").setServerInfo(getLocation()); |
| 448 } finally { | 321 } finally { |
| 449 if (c != null) { | 322 new FileUtils(sessionContext.getLog(), this).closeQuietly(os); |
| 450 c.disconnect(); | 323 remote.sessionEnd(); |
| 451 } | |
| 452 } | 324 } |
| 453 } | 325 } |
| 454 | 326 |
| 455 public Bookmarks getBookmarks() throws HgRemoteConnectionException, HgRuntimeException { | 327 public Bookmarks getBookmarks() throws HgRemoteConnectionException, HgRuntimeException { |
| 328 initCapabilities(); | |
| 329 if (!remoteCapabilities.contains(CMD_PUSHKEY)) { // (sic!) listkeys is available when pushkey in caps | |
| 330 return new Bookmarks(Collections.<Pair<String, Nodeid>>emptyList()); | |
| 331 } | |
| 456 final String actionName = "Get remote bookmarks"; | 332 final String actionName = "Get remote bookmarks"; |
| 457 final List<Pair<String, String>> values = listkeys("bookmarks", actionName); | 333 final List<Pair<String, String>> values = listkeys("bookmarks", actionName); |
| 458 ArrayList<Pair<String, Nodeid>> rv = new ArrayList<Pair<String, Nodeid>>(); | 334 ArrayList<Pair<String, Nodeid>> rv = new ArrayList<Pair<String, Nodeid>>(); |
| 459 for (Pair<String, String> l : values) { | 335 for (Pair<String, String> l : values) { |
| 460 if (l.second().length() != Nodeid.SIZE_ASCII) { | 336 if (l.second().length() != Nodeid.SIZE_ASCII) { |
| 468 return new Bookmarks(rv); | 344 return new Bookmarks(rv); |
| 469 } | 345 } |
| 470 | 346 |
| 471 public Outcome updateBookmark(String name, Nodeid oldRev, Nodeid newRev) throws HgRemoteConnectionException, HgRuntimeException { | 347 public Outcome updateBookmark(String name, Nodeid oldRev, Nodeid newRev) throws HgRemoteConnectionException, HgRuntimeException { |
| 472 initCapabilities(); | 348 initCapabilities(); |
| 473 if (!remoteCapabilities.contains("pushkey")) { | 349 if (!remoteCapabilities.contains(CMD_PUSHKEY)) { |
| 474 return new Outcome(Failure, "Server doesn't support pushkey protocol"); | 350 return new Outcome(Failure, "Server doesn't support pushkey protocol"); |
| 475 } | 351 } |
| 476 if (pushkey("Update remote bookmark", "bookmarks", name, oldRev.toString(), newRev.toString())) { | 352 if (pushkey("Update remote bookmark", NS_BOOKMARKS, name, oldRev.toString(), newRev.toString())) { |
| 477 return new Outcome(Success, String.format("Bookmark %s updated to %s", name, newRev.shortNotation())); | 353 return new Outcome(Success, String.format("Bookmark %s updated to %s", name, newRev.shortNotation())); |
| 478 } | 354 } |
| 479 return new Outcome(Failure, String.format("Bookmark update (%s: %s -> %s) failed", name, oldRev.shortNotation(), newRev.shortNotation())); | 355 return new Outcome(Failure, String.format("Bookmark update (%s: %s -> %s) failed", name, oldRev.shortNotation(), newRev.shortNotation())); |
| 480 } | 356 } |
| 481 | 357 |
| 482 public Phases getPhases() throws HgRemoteConnectionException, HgRuntimeException { | 358 public Phases getPhases() throws HgRemoteConnectionException, HgRuntimeException { |
| 483 initCapabilities(); | 359 initCapabilities(); |
| 484 if (!remoteCapabilities.contains("pushkey")) { | 360 if (!remoteCapabilities.contains(CMD_PUSHKEY)) { |
| 485 // old server defaults to publishing | 361 // old server defaults to publishing |
| 486 return new Phases(true, Collections.<Nodeid>emptyList()); | 362 return new Phases(true, Collections.<Nodeid>emptyList()); |
| 487 } | 363 } |
| 488 final List<Pair<String, String>> values = listkeys("phases", "Get remote phases"); | 364 final List<Pair<String, String>> values = listkeys(NS_PHASES, "Get remote phases"); |
| 489 boolean publishing = false; | 365 boolean publishing = false; |
| 490 ArrayList<Nodeid> draftRoots = new ArrayList<Nodeid>(); | 366 ArrayList<Nodeid> draftRoots = new ArrayList<Nodeid>(); |
| 491 for (Pair<String, String> l : values) { | 367 for (Pair<String, String> l : values) { |
| 492 if ("publishing".equalsIgnoreCase(l.first())) { | 368 if ("publishing".equalsIgnoreCase(l.first())) { |
| 493 publishing = Boolean.parseBoolean(l.second()); | 369 publishing = Boolean.parseBoolean(l.second()); |
| 505 return new Phases(publishing, draftRoots); | 381 return new Phases(publishing, draftRoots); |
| 506 } | 382 } |
| 507 | 383 |
| 508 public Outcome updatePhase(HgPhase from, HgPhase to, Nodeid n) throws HgRemoteConnectionException, HgRuntimeException { | 384 public Outcome updatePhase(HgPhase from, HgPhase to, Nodeid n) throws HgRemoteConnectionException, HgRuntimeException { |
| 509 initCapabilities(); | 385 initCapabilities(); |
| 510 if (!remoteCapabilities.contains("pushkey")) { | 386 if (!remoteCapabilities.contains(CMD_PUSHKEY)) { |
| 511 return new Outcome(Failure, "Server doesn't support pushkey protocol"); | 387 return new Outcome(Failure, "Server doesn't support pushkey protocol"); |
| 512 } | 388 } |
| 513 if (pushkey("Update remote phases", "phases", n.toString(), String.valueOf(from.mercurialOrdinal()), String.valueOf(to.mercurialOrdinal()))) { | 389 if (pushkey("Update remote phases", NS_PHASES, n.toString(), String.valueOf(from.mercurialOrdinal()), String.valueOf(to.mercurialOrdinal()))) { |
| 514 return new Outcome(Success, String.format("Phase of %s updated to %s", n.shortNotation(), to.name())); | 390 return new Outcome(Success, String.format("Phase of %s updated to %s", n.shortNotation(), to.name())); |
| 515 } | 391 } |
| 516 return new Outcome(Failure, String.format("Phase update (%s: %s -> %s) failed", n.shortNotation(), from.name(), to.name())); | 392 return new Outcome(Failure, String.format("Phase update (%s: %s -> %s) failed", n.shortNotation(), from.name(), to.name())); |
| 517 } | 393 } |
| 518 | 394 |
| 521 return getClass().getSimpleName() + '[' + getLocation() + ']'; | 397 return getClass().getSimpleName() + '[' + getLocation() + ']'; |
| 522 } | 398 } |
| 523 | 399 |
| 524 | 400 |
| 525 private void initCapabilities() throws HgRemoteConnectionException { | 401 private void initCapabilities() throws HgRemoteConnectionException { |
| 526 if (remoteCapabilities == null) { | 402 if (remoteCapabilities != null) { |
| 527 remoteCapabilities = new HashSet<String>(); | 403 return; |
| 528 // say hello to server, check response | 404 } |
| 529 try { | 405 remote.connect(); |
| 530 URL u = new URL(url, url.getPath() + "?cmd=hello"); | 406 try { |
| 531 HttpURLConnection c = setupConnection(u.openConnection()); | 407 remote.sessionBegin(); |
| 532 c.connect(); | 408 String capsLine = remote.getCapabilities(); |
| 533 if (debug) { | 409 String[] caps = capsLine.split("\\s"); |
| 534 dumpResponseHeader(u, c); | 410 remoteCapabilities = new HashSet<String>(Arrays.asList(caps)); |
| 535 } | 411 } finally { |
| 536 BufferedReader r = new BufferedReader(new InputStreamReader(c.getInputStream(), "US-ASCII")); | 412 remote.sessionEnd(); |
| 537 String line = r.readLine(); | |
| 538 c.disconnect(); | |
| 539 final String capsPrefix = "capabilities:"; | |
| 540 if (line == null || !line.startsWith(capsPrefix)) { | |
| 541 // for whatever reason, some servers do not respond to hello command (e.g. svnkit) | |
| 542 // but respond to 'capabilities' instead. Try it. | |
| 543 // TODO [post-1.0] tests needed | |
| 544 u = new URL(url, url.getPath() + "?cmd=capabilities"); | |
| 545 c = setupConnection(u.openConnection()); | |
| 546 c.connect(); | |
| 547 if (debug) { | |
| 548 dumpResponseHeader(u, c); | |
| 549 } | |
| 550 r = new BufferedReader(new InputStreamReader(c.getInputStream(), "US-ASCII")); | |
| 551 line = r.readLine(); | |
| 552 c.disconnect(); | |
| 553 if (line == null || line.trim().length() == 0) { | |
| 554 return; | |
| 555 } | |
| 556 } else { | |
| 557 line = line.substring(capsPrefix.length()).trim(); | |
| 558 } | |
| 559 String[] caps = line.split("\\s"); | |
| 560 remoteCapabilities.addAll(Arrays.asList(caps)); | |
| 561 c.disconnect(); | |
| 562 } catch (MalformedURLException ex) { | |
| 563 throw new HgRemoteConnectionException("Bad URL", ex).setRemoteCommand("hello").setServerInfo(getLocation()); | |
| 564 } catch (IOException ex) { | |
| 565 throw new HgRemoteConnectionException("Communication failure", ex).setRemoteCommand("hello").setServerInfo(getLocation()); | |
| 566 } | |
| 567 } | 413 } |
| 568 } | 414 } |
| 569 | 415 |
| 570 private HgLookup getLookupHelper() { | 416 private HgLookup getLookupHelper() { |
| 571 if (lookupHelper == null) { | 417 if (lookupHelper == null) { |
| 573 } | 419 } |
| 574 return lookupHelper; | 420 return lookupHelper; |
| 575 } | 421 } |
| 576 | 422 |
| 577 private List<Pair<String,String>> listkeys(String namespace, String actionName) throws HgRemoteConnectionException, HgRuntimeException { | 423 private List<Pair<String,String>> listkeys(String namespace, String actionName) throws HgRemoteConnectionException, HgRuntimeException { |
| 578 HttpURLConnection c = null; | 424 try { |
| 579 try { | 425 remote.sessionBegin(); |
| 580 URL u = new URL(url, url.getPath() + "?cmd=listkeys&namespace=" + namespace); | |
| 581 c = setupConnection(u.openConnection()); | |
| 582 c.connect(); | |
| 583 if (debug) { | |
| 584 dumpResponseHeader(u, c); | |
| 585 } | |
| 586 checkResponseOk(c, actionName, "listkeys"); | |
| 587 ArrayList<Pair<String, String>> rv = new ArrayList<Pair<String, String>>(); | 426 ArrayList<Pair<String, String>> rv = new ArrayList<Pair<String, String>>(); |
| 427 InputStream response = remote.listkeys(namespace, actionName); | |
| 588 // output of listkeys is encoded with UTF-8 | 428 // output of listkeys is encoded with UTF-8 |
| 589 BufferedReader r = new BufferedReader(new InputStreamReader(c.getInputStream(), EncodingHelper.getUTF8())); | 429 BufferedReader r = new BufferedReader(new InputStreamReader(response, EncodingHelper.getUTF8())); |
| 590 String l; | 430 String l; |
| 591 while ((l = r.readLine()) != null) { | 431 while ((l = r.readLine()) != null) { |
| 592 int sep = l.indexOf('\t'); | 432 int sep = l.indexOf('\t'); |
| 593 if (sep == -1) { | 433 if (sep == -1) { |
| 594 sessionContext.getLog().dump(getClass(), Severity.Warn, "%s: bad line '%s', ignored", actionName, l); | 434 sessionContext.getLog().dump(getClass(), Severity.Warn, "%s: bad line '%s', ignored", actionName, l); |
| 596 } | 436 } |
| 597 rv.add(new Pair<String,String>(l.substring(0, sep), l.substring(sep+1))); | 437 rv.add(new Pair<String,String>(l.substring(0, sep), l.substring(sep+1))); |
| 598 } | 438 } |
| 599 r.close(); | 439 r.close(); |
| 600 return rv; | 440 return rv; |
| 601 } catch (MalformedURLException ex) { | 441 } catch (IOException ex) { |
| 602 throw new HgRemoteConnectionException("Bad URL", ex).setRemoteCommand("listkeys").setServerInfo(getLocation()); | 442 throw new HgRemoteConnectionException("Communication failure", ex).setRemoteCommand(CMD_LISTKEYS).setServerInfo(getLocation()); |
| 603 } catch (IOException ex) { | 443 } finally { |
| 604 throw new HgRemoteConnectionException("Communication failure", ex).setRemoteCommand("listkeys").setServerInfo(getLocation()); | 444 remote.sessionEnd(); |
| 605 } finally { | |
| 606 if (c != null) { | |
| 607 c.disconnect(); | |
| 608 } | |
| 609 } | 445 } |
| 610 } | 446 } |
| 611 | 447 |
| 612 private boolean pushkey(String opName, String namespace, String key, String oldValue, String newValue) throws HgRemoteConnectionException, HgRuntimeException { | 448 private boolean pushkey(String opName, String namespace, String key, String oldValue, String newValue) throws HgRemoteConnectionException, HgRuntimeException { |
| 613 HttpURLConnection c = null; | 449 try { |
| 614 try { | 450 remote.sessionBegin(); |
| 615 final String p = String.format("%s?cmd=pushkey&namespace=%s&key=%s&old=%s&new=%s", url.getPath(), namespace, key, oldValue, newValue); | 451 final InputStream is = remote.pushkey(opName, namespace, key, oldValue, newValue); |
| 616 URL u = new URL(url, p); | |
| 617 c = setupConnection(u.openConnection()); | |
| 618 c.setRequestMethod("POST"); | |
| 619 c.connect(); | |
| 620 if (debug) { | |
| 621 dumpResponseHeader(u, c); | |
| 622 } | |
| 623 checkResponseOk(c, opName, "pushkey"); | |
| 624 final InputStream is = c.getInputStream(); | |
| 625 int rv = is.read(); | 452 int rv = is.read(); |
| 626 is.close(); | 453 is.close(); |
| 627 return rv == '1'; | 454 return rv == '1'; |
| 628 } catch (MalformedURLException ex) { | 455 } catch (IOException ex) { |
| 629 throw new HgRemoteConnectionException("Bad URL", ex).setRemoteCommand("pushkey").setServerInfo(getLocation()); | 456 throw new HgRemoteConnectionException("Communication failure", ex).setRemoteCommand(CMD_PUSHKEY).setServerInfo(getLocation()); |
| 630 } catch (IOException ex) { | 457 } finally { |
| 631 throw new HgRemoteConnectionException("Communication failure", ex).setRemoteCommand("pushkey").setServerInfo(getLocation()); | 458 remote.sessionEnd(); |
| 632 } finally { | |
| 633 if (c != null) { | |
| 634 c.disconnect(); | |
| 635 } | |
| 636 } | |
| 637 } | |
| 638 | |
| 639 private void checkResponseOk(HttpURLConnection c, String opName, String remoteCmd) throws HgRemoteConnectionException, IOException { | |
| 640 if (c.getResponseCode() != 200) { | |
| 641 String m = c.getResponseMessage() == null ? "unknown reason" : c.getResponseMessage(); | |
| 642 String em = String.format("%s failed: %s (HTTP error:%d)", opName, m, c.getResponseCode()); | |
| 643 throw new HgRemoteConnectionException(em).setRemoteCommand(remoteCmd).setServerInfo(getLocation()); | |
| 644 } | |
| 645 } | |
| 646 | |
| 647 private HttpURLConnection setupConnection(URLConnection urlConnection) { | |
| 648 urlConnection.setRequestProperty("User-Agent", "hg4j/1.0.0"); | |
| 649 urlConnection.addRequestProperty("Accept", "application/mercurial-0.1"); | |
| 650 if (authInfo != null) { | |
| 651 urlConnection.addRequestProperty("Authorization", "Basic " + authInfo); | |
| 652 } | |
| 653 if (sslContext != null) { | |
| 654 ((HttpsURLConnection) urlConnection).setSSLSocketFactory(sslContext.getSocketFactory()); | |
| 655 } | |
| 656 return (HttpURLConnection) urlConnection; | |
| 657 } | |
| 658 | |
| 659 private StringBuilder appendNodeidListArgument(String key, List<Nodeid> values, StringBuilder sb) { | |
| 660 if (sb == null) { | |
| 661 sb = new StringBuilder(20 + values.size() * 41); | |
| 662 } | |
| 663 sb.append(key); | |
| 664 sb.append('='); | |
| 665 for (Nodeid n : values) { | |
| 666 sb.append(n.toString()); | |
| 667 sb.append('+'); | |
| 668 } | |
| 669 if (sb.charAt(sb.length() - 1) == '+') { | |
| 670 // strip last space | |
| 671 sb.setLength(sb.length() - 1); | |
| 672 } | |
| 673 return sb; | |
| 674 } | |
| 675 | |
| 676 private void dumpResponseHeader(URL u, HttpURLConnection c) { | |
| 677 System.out.printf("Query (%d bytes):%s\n", u.getQuery().length(), u.getQuery()); | |
| 678 System.out.println("Response headers:"); | |
| 679 final Map<String, List<String>> headerFields = c.getHeaderFields(); | |
| 680 for (String s : headerFields.keySet()) { | |
| 681 System.out.printf("%s: %s\n", s, c.getHeaderField(s)); | |
| 682 } | |
| 683 } | |
| 684 | |
| 685 private void dumpResponse(HttpURLConnection c) throws IOException { | |
| 686 if (c.getContentLength() > 0) { | |
| 687 final Object content = c.getContent(); | |
| 688 System.out.println(content); | |
| 689 } | 459 } |
| 690 } | 460 } |
| 691 | 461 |
| 692 private static File writeBundle(InputStream is, boolean decompress, String header) throws IOException { | 462 private static File writeBundle(InputStream is, boolean decompress, String header) throws IOException { |
| 693 InputStream zipStream = decompress ? new InflaterInputStream(is) : is; | 463 InputStream zipStream = decompress ? new InflaterInputStream(is) : is; |
