Mercurial > jhg
comparison hg4j/src/main/java/org/tmatesoft/hg/repo/HgRemoteRepository.java @ 213:6ec4af642ba8 gradle
Project uses Gradle for build - actual changes
| author | Alexander Kitaev <kitaev@gmail.com> |
|---|---|
| date | Tue, 10 May 2011 10:52:53 +0200 |
| parents | |
| children |
comparison
equal
deleted
inserted
replaced
| 212:edb2e2829352 | 213:6ec4af642ba8 |
|---|---|
| 1 /* | |
| 2 * Copyright (c) 2011 TMate Software Ltd | |
| 3 * | |
| 4 * This program is free software; you can redistribute it and/or modify | |
| 5 * it under the terms of the GNU General Public License as published by | |
| 6 * the Free Software Foundation; version 2 of the License. | |
| 7 * | |
| 8 * This program is distributed in the hope that it will be useful, | |
| 9 * but WITHOUT ANY WARRANTY; without even the implied warranty of | |
| 10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
| 11 * GNU General Public License for more details. | |
| 12 * | |
| 13 * For information on how to redistribute this software under | |
| 14 * the terms of a license other than GNU General Public License | |
| 15 * contact TMate Software at support@hg4j.com | |
| 16 */ | |
| 17 package org.tmatesoft.hg.repo; | |
| 18 | |
| 19 import java.io.File; | |
| 20 import java.io.FileOutputStream; | |
| 21 import java.io.IOException; | |
| 22 import java.io.InputStream; | |
| 23 import java.io.InputStreamReader; | |
| 24 import java.io.OutputStream; | |
| 25 import java.io.StreamTokenizer; | |
| 26 import java.net.HttpURLConnection; | |
| 27 import java.net.MalformedURLException; | |
| 28 import java.net.URL; | |
| 29 import java.net.URLConnection; | |
| 30 import java.security.cert.CertificateException; | |
| 31 import java.security.cert.X509Certificate; | |
| 32 import java.util.ArrayList; | |
| 33 import java.util.Collection; | |
| 34 import java.util.Collections; | |
| 35 import java.util.Iterator; | |
| 36 import java.util.LinkedHashMap; | |
| 37 import java.util.LinkedList; | |
| 38 import java.util.List; | |
| 39 import java.util.Map; | |
| 40 import java.util.prefs.BackingStoreException; | |
| 41 import java.util.prefs.Preferences; | |
| 42 import java.util.zip.InflaterInputStream; | |
| 43 | |
| 44 import javax.net.ssl.HttpsURLConnection; | |
| 45 import javax.net.ssl.SSLContext; | |
| 46 import javax.net.ssl.TrustManager; | |
| 47 import javax.net.ssl.X509TrustManager; | |
| 48 | |
| 49 import org.tmatesoft.hg.core.HgBadArgumentException; | |
| 50 import org.tmatesoft.hg.core.HgBadStateException; | |
| 51 import org.tmatesoft.hg.core.HgException; | |
| 52 import org.tmatesoft.hg.core.Nodeid; | |
| 53 | |
| 54 /** | |
| 55 * WORK IN PROGRESS, DO NOT USE | |
| 56 * | |
| 57 * @see http://mercurial.selenic.com/wiki/WireProtocol | |
| 58 * | |
| 59 * @author Artem Tikhomirov | |
| 60 * @author TMate Software Ltd. | |
| 61 */ | |
| 62 public class HgRemoteRepository { | |
| 63 | |
| 64 private final URL url; | |
| 65 private final SSLContext sslContext; | |
| 66 private final String authInfo; | |
| 67 private final boolean debug = Boolean.parseBoolean(System.getProperty("hg4j.remote.debug")); | |
| 68 private HgLookup lookupHelper; | |
| 69 | |
| 70 HgRemoteRepository(URL url) throws HgBadArgumentException { | |
| 71 if (url == null) { | |
| 72 throw new IllegalArgumentException(); | |
| 73 } | |
| 74 this.url = url; | |
| 75 if ("https".equals(url.getProtocol())) { | |
| 76 try { | |
| 77 sslContext = SSLContext.getInstance("SSL"); | |
| 78 class TrustEveryone implements X509TrustManager { | |
| 79 public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { | |
| 80 if (debug) { | |
| 81 System.out.println("checkClientTrusted:" + authType); | |
| 82 } | |
| 83 } | |
| 84 public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { | |
| 85 if (debug) { | |
| 86 System.out.println("checkServerTrusted:" + authType); | |
| 87 } | |
| 88 } | |
| 89 public X509Certificate[] getAcceptedIssuers() { | |
| 90 return new X509Certificate[0]; | |
| 91 } | |
| 92 }; | |
| 93 sslContext.init(null, new TrustManager[] { new TrustEveryone() }, null); | |
| 94 } catch (Exception ex) { | |
| 95 throw new HgBadArgumentException("Can't initialize secure connection", ex); | |
| 96 } | |
| 97 } else { | |
| 98 sslContext = null; | |
| 99 } | |
| 100 if (url.getUserInfo() != null) { | |
| 101 String ai = null; | |
| 102 try { | |
| 103 // Hack to get Base64-encoded credentials | |
| 104 Preferences tempNode = Preferences.userRoot().node("xxx"); | |
| 105 tempNode.putByteArray("xxx", url.getUserInfo().getBytes()); | |
| 106 ai = tempNode.get("xxx", null); | |
| 107 tempNode.removeNode(); | |
| 108 } catch (BackingStoreException ex) { | |
| 109 ex.printStackTrace(); | |
| 110 // IGNORE | |
| 111 } | |
| 112 authInfo = ai; | |
| 113 } else { | |
| 114 authInfo = null; | |
| 115 } | |
| 116 } | |
| 117 | |
| 118 public boolean isInvalid() throws HgException { | |
| 119 // say hello to server, check response | |
| 120 if (Boolean.FALSE.booleanValue()) { | |
| 121 throw HgRepository.notImplemented(); | |
| 122 } | |
| 123 return false; // FIXME | |
| 124 } | |
| 125 | |
| 126 /** | |
| 127 * @return human-readable address of the server, without user credentials or any other security information | |
| 128 */ | |
| 129 public String getLocation() { | |
| 130 if (url.getUserInfo() == null) { | |
| 131 return url.toExternalForm(); | |
| 132 } | |
| 133 if (url.getPort() != -1) { | |
| 134 return String.format("%s://%s:%d%s", url.getProtocol(), url.getHost(), url.getPort(), url.getPath()); | |
| 135 } else { | |
| 136 return String.format("%s://%s%s", url.getProtocol(), url.getHost(), url.getPath()); | |
| 137 } | |
| 138 } | |
| 139 | |
| 140 public List<Nodeid> heads() throws HgException { | |
| 141 try { | |
| 142 URL u = new URL(url, url.getPath() + "?cmd=heads"); | |
| 143 HttpURLConnection c = setupConnection(u.openConnection()); | |
| 144 c.connect(); | |
| 145 if (debug) { | |
| 146 dumpResponseHeader(u, c); | |
| 147 } | |
| 148 InputStreamReader is = new InputStreamReader(c.getInputStream(), "US-ASCII"); | |
| 149 StreamTokenizer st = new StreamTokenizer(is); | |
| 150 st.ordinaryChars('0', '9'); | |
| 151 st.wordChars('0', '9'); | |
| 152 st.eolIsSignificant(false); | |
| 153 LinkedList<Nodeid> parseResult = new LinkedList<Nodeid>(); | |
| 154 while (st.nextToken() != StreamTokenizer.TT_EOF) { | |
| 155 parseResult.add(Nodeid.fromAscii(st.sval)); | |
| 156 } | |
| 157 return parseResult; | |
| 158 } catch (MalformedURLException ex) { | |
| 159 throw new HgException(ex); | |
| 160 } catch (IOException ex) { | |
| 161 throw new HgException(ex); | |
| 162 } | |
| 163 } | |
| 164 | |
| 165 public List<Nodeid> between(Nodeid tip, Nodeid base) throws HgException { | |
| 166 Range r = new Range(base, tip); | |
| 167 // XXX shall handle errors like no range key in the returned map, not sure how. | |
| 168 return between(Collections.singletonList(r)).get(r); | |
| 169 } | |
| 170 | |
| 171 /** | |
| 172 * @param ranges | |
| 173 * @return map, where keys are input instances, values are corresponding server reply | |
| 174 * @throws HgException | |
| 175 */ | |
| 176 public Map<Range, List<Nodeid>> between(Collection<Range> ranges) throws HgException { | |
| 177 if (ranges.isEmpty()) { | |
| 178 return Collections.emptyMap(); | |
| 179 } | |
| 180 // if fact, shall do other way round, this method shall send | |
| 181 LinkedHashMap<Range, List<Nodeid>> rv = new LinkedHashMap<HgRemoteRepository.Range, List<Nodeid>>(ranges.size() * 4 / 3); | |
| 182 StringBuilder sb = new StringBuilder(20 + ranges.size() * 82); | |
| 183 sb.append("pairs="); | |
| 184 for (Range r : ranges) { | |
| 185 sb.append(r.end.toString()); | |
| 186 sb.append('-'); | |
| 187 sb.append(r.start.toString()); | |
| 188 sb.append('+'); | |
| 189 } | |
| 190 if (sb.charAt(sb.length() - 1) == '+') { | |
| 191 // strip last space | |
| 192 sb.setLength(sb.length() - 1); | |
| 193 } | |
| 194 try { | |
| 195 boolean usePOST = ranges.size() > 3; | |
| 196 URL u = new URL(url, url.getPath() + "?cmd=between" + (usePOST ? "" : '&' + sb.toString())); | |
| 197 HttpURLConnection c = setupConnection(u.openConnection()); | |
| 198 if (usePOST) { | |
| 199 c.setRequestMethod("POST"); | |
| 200 c.setRequestProperty("Content-Length", String.valueOf(sb.length()/*nodeids are ASCII, bytes == characters */)); | |
| 201 c.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); | |
| 202 c.setDoOutput(true); | |
| 203 c.connect(); | |
| 204 OutputStream os = c.getOutputStream(); | |
| 205 os.write(sb.toString().getBytes()); | |
| 206 os.flush(); | |
| 207 os.close(); | |
| 208 } else { | |
| 209 c.connect(); | |
| 210 } | |
| 211 if (debug) { | |
| 212 System.out.printf("%d ranges, method:%s \n", ranges.size(), c.getRequestMethod()); | |
| 213 dumpResponseHeader(u, c); | |
| 214 } | |
| 215 InputStreamReader is = new InputStreamReader(c.getInputStream(), "US-ASCII"); | |
| 216 StreamTokenizer st = new StreamTokenizer(is); | |
| 217 st.ordinaryChars('0', '9'); | |
| 218 st.wordChars('0', '9'); | |
| 219 st.eolIsSignificant(true); | |
| 220 Iterator<Range> rangeItr = ranges.iterator(); | |
| 221 LinkedList<Nodeid> currRangeList = null; | |
| 222 Range currRange = null; | |
| 223 boolean possiblyEmptyNextLine = true; | |
| 224 while (st.nextToken() != StreamTokenizer.TT_EOF) { | |
| 225 if (st.ttype == StreamTokenizer.TT_EOL) { | |
| 226 if (possiblyEmptyNextLine) { | |
| 227 // newline follows newline; | |
| 228 assert currRange == null; | |
| 229 assert currRangeList == null; | |
| 230 if (!rangeItr.hasNext()) { | |
| 231 throw new HgBadStateException(); | |
| 232 } | |
| 233 rv.put(rangeItr.next(), Collections.<Nodeid>emptyList()); | |
| 234 } else { | |
| 235 if (currRange == null || currRangeList == null) { | |
| 236 throw new HgBadStateException(); | |
| 237 } | |
| 238 // indicate next range value is needed | |
| 239 currRange = null; | |
| 240 currRangeList = null; | |
| 241 possiblyEmptyNextLine = true; | |
| 242 } | |
| 243 } else { | |
| 244 possiblyEmptyNextLine = false; | |
| 245 if (currRange == null) { | |
| 246 if (!rangeItr.hasNext()) { | |
| 247 throw new HgBadStateException(); | |
| 248 } | |
| 249 currRange = rangeItr.next(); | |
| 250 currRangeList = new LinkedList<Nodeid>(); | |
| 251 rv.put(currRange, currRangeList); | |
| 252 } | |
| 253 Nodeid nid = Nodeid.fromAscii(st.sval); | |
| 254 currRangeList.addLast(nid); | |
| 255 } | |
| 256 } | |
| 257 is.close(); | |
| 258 return rv; | |
| 259 } catch (MalformedURLException ex) { | |
| 260 throw new HgException(ex); | |
| 261 } catch (IOException ex) { | |
| 262 throw new HgException(ex); | |
| 263 } | |
| 264 } | |
| 265 | |
| 266 public List<RemoteBranch> branches(List<Nodeid> nodes) throws HgException { | |
| 267 StringBuilder sb = new StringBuilder(20 + nodes.size() * 41); | |
| 268 sb.append("nodes="); | |
| 269 for (Nodeid n : nodes) { | |
| 270 sb.append(n.toString()); | |
| 271 sb.append('+'); | |
| 272 } | |
| 273 if (sb.charAt(sb.length() - 1) == '+') { | |
| 274 // strip last space | |
| 275 sb.setLength(sb.length() - 1); | |
| 276 } | |
| 277 try { | |
| 278 URL u = new URL(url, url.getPath() + "?cmd=branches&" + sb.toString()); | |
| 279 HttpURLConnection c = setupConnection(u.openConnection()); | |
| 280 c.connect(); | |
| 281 if (debug) { | |
| 282 dumpResponseHeader(u, c); | |
| 283 } | |
| 284 InputStreamReader is = new InputStreamReader(c.getInputStream(), "US-ASCII"); | |
| 285 StreamTokenizer st = new StreamTokenizer(is); | |
| 286 st.ordinaryChars('0', '9'); | |
| 287 st.wordChars('0', '9'); | |
| 288 st.eolIsSignificant(false); | |
| 289 ArrayList<Nodeid> parseResult = new ArrayList<Nodeid>(nodes.size() * 4); | |
| 290 while (st.nextToken() != StreamTokenizer.TT_EOF) { | |
| 291 parseResult.add(Nodeid.fromAscii(st.sval)); | |
| 292 } | |
| 293 if (parseResult.size() != nodes.size() * 4) { | |
| 294 throw new HgException(String.format("Bad number of nodeids in result (shall be factor 4), expected %d, got %d", nodes.size()*4, parseResult.size())); | |
| 295 } | |
| 296 ArrayList<RemoteBranch> rv = new ArrayList<RemoteBranch>(nodes.size()); | |
| 297 for (int i = 0; i < nodes.size(); i++) { | |
| 298 RemoteBranch rb = new RemoteBranch(parseResult.get(i*4), parseResult.get(i*4 + 1), parseResult.get(i*4 + 2), parseResult.get(i*4 + 3)); | |
| 299 rv.add(rb); | |
| 300 } | |
| 301 return rv; | |
| 302 } catch (MalformedURLException ex) { | |
| 303 throw new HgException(ex); | |
| 304 } catch (IOException ex) { | |
| 305 throw new HgException(ex); | |
| 306 } | |
| 307 } | |
| 308 | |
| 309 /* | |
| 310 * XXX need to describe behavior when roots arg is empty; our RepositoryComparator code currently returns empty lists when | |
| 311 * no common elements found, which in turn means we need to query changes starting with NULL nodeid. | |
| 312 * | |
| 313 * WireProtocol wiki: roots = a list of the latest nodes on every service side changeset branch that both the client and server know about. | |
| 314 * | |
| 315 * Perhaps, shall be named 'changegroup' | |
| 316 | |
| 317 * Changegroup: | |
| 318 * http://mercurial.selenic.com/wiki/Merge | |
| 319 * http://mercurial.selenic.com/wiki/WireProtocol | |
| 320 * | |
| 321 * according to latter, bundleformat data is sent through zlib | |
| 322 * (there's no header like HG10?? with the server output, though, | |
| 323 * as one may expect according to http://mercurial.selenic.com/wiki/BundleFormat) | |
| 324 */ | |
| 325 public HgBundle getChanges(List<Nodeid> roots) throws HgException { | |
| 326 List<Nodeid> _roots = roots.isEmpty() ? Collections.singletonList(Nodeid.NULL) : roots; | |
| 327 StringBuilder sb = new StringBuilder(20 + _roots.size() * 41); | |
| 328 sb.append("roots="); | |
| 329 for (Nodeid n : _roots) { | |
| 330 sb.append(n.toString()); | |
| 331 sb.append('+'); | |
| 332 } | |
| 333 if (sb.charAt(sb.length() - 1) == '+') { | |
| 334 // strip last space | |
| 335 sb.setLength(sb.length() - 1); | |
| 336 } | |
| 337 try { | |
| 338 URL u = new URL(url, url.getPath() + "?cmd=changegroup&" + sb.toString()); | |
| 339 HttpURLConnection c = setupConnection(u.openConnection()); | |
| 340 c.connect(); | |
| 341 if (debug) { | |
| 342 dumpResponseHeader(u, c); | |
| 343 } | |
| 344 File tf = writeBundle(c.getInputStream(), false, "HG10GZ" /*didn't see any other that zip*/); | |
| 345 if (debug) { | |
| 346 System.out.printf("Wrote bundle %s for roots %s\n", tf, sb); | |
| 347 } | |
| 348 return getLookupHelper().loadBundle(tf); | |
| 349 } catch (MalformedURLException ex) { | |
| 350 throw new HgException(ex); | |
| 351 } catch (IOException ex) { | |
| 352 throw new HgException(ex); | |
| 353 } | |
| 354 } | |
| 355 | |
| 356 @Override | |
| 357 public String toString() { | |
| 358 return getClass().getSimpleName() + '[' + getLocation() + ']'; | |
| 359 } | |
| 360 | |
| 361 private HgLookup getLookupHelper() { | |
| 362 if (lookupHelper == null) { | |
| 363 lookupHelper = new HgLookup(); | |
| 364 } | |
| 365 return lookupHelper; | |
| 366 } | |
| 367 | |
| 368 private HttpURLConnection setupConnection(URLConnection urlConnection) { | |
| 369 urlConnection.setRequestProperty("User-Agent", "hg4j/0.5.0"); | |
| 370 urlConnection.addRequestProperty("Accept", "application/mercurial-0.1"); | |
| 371 if (authInfo != null) { | |
| 372 urlConnection.addRequestProperty("Authorization", "Basic " + authInfo); | |
| 373 } | |
| 374 if (sslContext != null) { | |
| 375 ((HttpsURLConnection) urlConnection).setSSLSocketFactory(sslContext.getSocketFactory()); | |
| 376 } | |
| 377 return (HttpURLConnection) urlConnection; | |
| 378 } | |
| 379 | |
| 380 private void dumpResponseHeader(URL u, HttpURLConnection c) { | |
| 381 System.out.printf("Query (%d bytes):%s\n", u.getQuery().length(), u.getQuery()); | |
| 382 System.out.println("Response headers:"); | |
| 383 final Map<String, List<String>> headerFields = c.getHeaderFields(); | |
| 384 for (String s : headerFields.keySet()) { | |
| 385 System.out.printf("%s: %s\n", s, c.getHeaderField(s)); | |
| 386 } | |
| 387 } | |
| 388 | |
| 389 private static File writeBundle(InputStream is, boolean decompress, String header) throws IOException { | |
| 390 InputStream zipStream = decompress ? new InflaterInputStream(is) : is; | |
| 391 File tf = File.createTempFile("hg-bundle-", null); | |
| 392 FileOutputStream fos = new FileOutputStream(tf); | |
| 393 fos.write(header.getBytes()); | |
| 394 int r; | |
| 395 byte[] buf = new byte[8*1024]; | |
| 396 while ((r = zipStream.read(buf)) != -1) { | |
| 397 fos.write(buf, 0, r); | |
| 398 } | |
| 399 fos.close(); | |
| 400 zipStream.close(); | |
| 401 return tf; | |
| 402 } | |
| 403 | |
| 404 | |
| 405 public static final class Range { | |
| 406 /** | |
| 407 * Root of the range, earlier revision | |
| 408 */ | |
| 409 public final Nodeid start; | |
| 410 /** | |
| 411 * Head of the range, later revision. | |
| 412 */ | |
| 413 public final Nodeid end; | |
| 414 | |
| 415 /** | |
| 416 * @param from - root/base revision | |
| 417 * @param to - head/tip revision | |
| 418 */ | |
| 419 public Range(Nodeid from, Nodeid to) { | |
| 420 start = from; | |
| 421 end = to; | |
| 422 } | |
| 423 } | |
| 424 | |
| 425 public static final class RemoteBranch { | |
| 426 public final Nodeid head, root, p1, p2; | |
| 427 | |
| 428 public RemoteBranch(Nodeid h, Nodeid r, Nodeid parent1, Nodeid parent2) { | |
| 429 head = h; | |
| 430 root = r; | |
| 431 p1 = parent1; | |
| 432 p2 = parent2; | |
| 433 } | |
| 434 | |
| 435 @Override | |
| 436 public boolean equals(Object obj) { | |
| 437 if (this == obj) { | |
| 438 return true; | |
| 439 } | |
| 440 if (false == obj instanceof RemoteBranch) { | |
| 441 return false; | |
| 442 } | |
| 443 RemoteBranch o = (RemoteBranch) obj; | |
| 444 return head.equals(o.head) && root.equals(o.root) && (p1 == null && o.p1 == null || p1.equals(o.p1)) && (p2 == null && o.p2 == null || p2.equals(o.p2)); | |
| 445 } | |
| 446 } | |
| 447 } |
