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;