Mercurial > jhg
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; |