kitaev@213: /* kitaev@213: * Copyright (c) 2011 TMate Software Ltd kitaev@213: * kitaev@213: * This program is free software; you can redistribute it and/or modify kitaev@213: * it under the terms of the GNU General Public License as published by kitaev@213: * the Free Software Foundation; version 2 of the License. kitaev@213: * kitaev@213: * This program is distributed in the hope that it will be useful, kitaev@213: * but WITHOUT ANY WARRANTY; without even the implied warranty of kitaev@213: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the kitaev@213: * GNU General Public License for more details. kitaev@213: * kitaev@213: * For information on how to redistribute this software under kitaev@213: * the terms of a license other than GNU General Public License kitaev@213: * contact TMate Software at support@hg4j.com kitaev@213: */ kitaev@213: package org.tmatesoft.hg.repo; kitaev@213: kitaev@213: import java.io.File; kitaev@213: import java.io.FileOutputStream; kitaev@213: import java.io.IOException; kitaev@213: import java.io.InputStream; kitaev@213: import java.io.InputStreamReader; kitaev@213: import java.io.OutputStream; kitaev@213: import java.io.StreamTokenizer; kitaev@213: import java.net.HttpURLConnection; kitaev@213: import java.net.MalformedURLException; kitaev@213: import java.net.URL; kitaev@213: import java.net.URLConnection; kitaev@213: import java.security.cert.CertificateException; kitaev@213: import java.security.cert.X509Certificate; kitaev@213: import java.util.ArrayList; kitaev@213: import java.util.Collection; kitaev@213: import java.util.Collections; kitaev@213: import java.util.Iterator; kitaev@213: import java.util.LinkedHashMap; kitaev@213: import java.util.LinkedList; kitaev@213: import java.util.List; kitaev@213: import java.util.Map; kitaev@213: import java.util.prefs.BackingStoreException; kitaev@213: import java.util.prefs.Preferences; kitaev@213: import java.util.zip.InflaterInputStream; kitaev@213: kitaev@213: import javax.net.ssl.HttpsURLConnection; kitaev@213: import javax.net.ssl.SSLContext; kitaev@213: import javax.net.ssl.TrustManager; kitaev@213: import javax.net.ssl.X509TrustManager; kitaev@213: kitaev@213: import org.tmatesoft.hg.core.HgBadArgumentException; kitaev@213: import org.tmatesoft.hg.core.HgBadStateException; kitaev@213: import org.tmatesoft.hg.core.HgException; kitaev@213: import org.tmatesoft.hg.core.Nodeid; kitaev@213: kitaev@213: /** kitaev@213: * WORK IN PROGRESS, DO NOT USE kitaev@213: * kitaev@213: * @see http://mercurial.selenic.com/wiki/WireProtocol kitaev@213: * kitaev@213: * @author Artem Tikhomirov kitaev@213: * @author TMate Software Ltd. kitaev@213: */ kitaev@213: public class HgRemoteRepository { kitaev@213: kitaev@213: private final URL url; kitaev@213: private final SSLContext sslContext; kitaev@213: private final String authInfo; kitaev@213: private final boolean debug = Boolean.parseBoolean(System.getProperty("hg4j.remote.debug")); kitaev@213: private HgLookup lookupHelper; kitaev@213: kitaev@213: HgRemoteRepository(URL url) throws HgBadArgumentException { kitaev@213: if (url == null) { kitaev@213: throw new IllegalArgumentException(); kitaev@213: } kitaev@213: this.url = url; kitaev@213: if ("https".equals(url.getProtocol())) { kitaev@213: try { kitaev@213: sslContext = SSLContext.getInstance("SSL"); kitaev@213: class TrustEveryone implements X509TrustManager { kitaev@213: public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { kitaev@213: if (debug) { kitaev@213: System.out.println("checkClientTrusted:" + authType); kitaev@213: } kitaev@213: } kitaev@213: public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { kitaev@213: if (debug) { kitaev@213: System.out.println("checkServerTrusted:" + authType); kitaev@213: } kitaev@213: } kitaev@213: public X509Certificate[] getAcceptedIssuers() { kitaev@213: return new X509Certificate[0]; kitaev@213: } kitaev@213: }; kitaev@213: sslContext.init(null, new TrustManager[] { new TrustEveryone() }, null); kitaev@213: } catch (Exception ex) { kitaev@213: throw new HgBadArgumentException("Can't initialize secure connection", ex); kitaev@213: } kitaev@213: } else { kitaev@213: sslContext = null; kitaev@213: } kitaev@213: if (url.getUserInfo() != null) { kitaev@213: String ai = null; kitaev@213: try { kitaev@213: // Hack to get Base64-encoded credentials kitaev@213: Preferences tempNode = Preferences.userRoot().node("xxx"); kitaev@213: tempNode.putByteArray("xxx", url.getUserInfo().getBytes()); kitaev@213: ai = tempNode.get("xxx", null); kitaev@213: tempNode.removeNode(); kitaev@213: } catch (BackingStoreException ex) { kitaev@213: ex.printStackTrace(); kitaev@213: // IGNORE kitaev@213: } kitaev@213: authInfo = ai; kitaev@213: } else { kitaev@213: authInfo = null; kitaev@213: } kitaev@213: } kitaev@213: kitaev@213: public boolean isInvalid() throws HgException { kitaev@213: // say hello to server, check response kitaev@213: if (Boolean.FALSE.booleanValue()) { kitaev@213: throw HgRepository.notImplemented(); kitaev@213: } kitaev@213: return false; // FIXME kitaev@213: } kitaev@213: kitaev@213: /** kitaev@213: * @return human-readable address of the server, without user credentials or any other security information kitaev@213: */ kitaev@213: public String getLocation() { kitaev@213: if (url.getUserInfo() == null) { kitaev@213: return url.toExternalForm(); kitaev@213: } kitaev@213: if (url.getPort() != -1) { kitaev@213: return String.format("%s://%s:%d%s", url.getProtocol(), url.getHost(), url.getPort(), url.getPath()); kitaev@213: } else { kitaev@213: return String.format("%s://%s%s", url.getProtocol(), url.getHost(), url.getPath()); kitaev@213: } kitaev@213: } kitaev@213: kitaev@213: public List heads() throws HgException { kitaev@213: try { kitaev@213: URL u = new URL(url, url.getPath() + "?cmd=heads"); kitaev@213: HttpURLConnection c = setupConnection(u.openConnection()); kitaev@213: c.connect(); kitaev@213: if (debug) { kitaev@213: dumpResponseHeader(u, c); kitaev@213: } kitaev@213: InputStreamReader is = new InputStreamReader(c.getInputStream(), "US-ASCII"); kitaev@213: StreamTokenizer st = new StreamTokenizer(is); kitaev@213: st.ordinaryChars('0', '9'); kitaev@213: st.wordChars('0', '9'); kitaev@213: st.eolIsSignificant(false); kitaev@213: LinkedList parseResult = new LinkedList(); kitaev@213: while (st.nextToken() != StreamTokenizer.TT_EOF) { kitaev@213: parseResult.add(Nodeid.fromAscii(st.sval)); kitaev@213: } kitaev@213: return parseResult; kitaev@213: } catch (MalformedURLException ex) { kitaev@213: throw new HgException(ex); kitaev@213: } catch (IOException ex) { kitaev@213: throw new HgException(ex); kitaev@213: } kitaev@213: } kitaev@213: kitaev@213: public List between(Nodeid tip, Nodeid base) throws HgException { kitaev@213: Range r = new Range(base, tip); kitaev@213: // XXX shall handle errors like no range key in the returned map, not sure how. kitaev@213: return between(Collections.singletonList(r)).get(r); kitaev@213: } kitaev@213: kitaev@213: /** kitaev@213: * @param ranges kitaev@213: * @return map, where keys are input instances, values are corresponding server reply kitaev@213: * @throws HgException kitaev@213: */ kitaev@213: public Map> between(Collection ranges) throws HgException { kitaev@213: if (ranges.isEmpty()) { kitaev@213: return Collections.emptyMap(); kitaev@213: } kitaev@213: // if fact, shall do other way round, this method shall send kitaev@213: LinkedHashMap> rv = new LinkedHashMap>(ranges.size() * 4 / 3); kitaev@213: StringBuilder sb = new StringBuilder(20 + ranges.size() * 82); kitaev@213: sb.append("pairs="); kitaev@213: for (Range r : ranges) { kitaev@213: sb.append(r.end.toString()); kitaev@213: sb.append('-'); kitaev@213: sb.append(r.start.toString()); kitaev@213: sb.append('+'); kitaev@213: } kitaev@213: if (sb.charAt(sb.length() - 1) == '+') { kitaev@213: // strip last space kitaev@213: sb.setLength(sb.length() - 1); kitaev@213: } kitaev@213: try { kitaev@213: boolean usePOST = ranges.size() > 3; kitaev@213: URL u = new URL(url, url.getPath() + "?cmd=between" + (usePOST ? "" : '&' + sb.toString())); kitaev@213: HttpURLConnection c = setupConnection(u.openConnection()); kitaev@213: if (usePOST) { kitaev@213: c.setRequestMethod("POST"); kitaev@213: c.setRequestProperty("Content-Length", String.valueOf(sb.length()/*nodeids are ASCII, bytes == characters */)); kitaev@213: c.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); kitaev@213: c.setDoOutput(true); kitaev@213: c.connect(); kitaev@213: OutputStream os = c.getOutputStream(); kitaev@213: os.write(sb.toString().getBytes()); kitaev@213: os.flush(); kitaev@213: os.close(); kitaev@213: } else { kitaev@213: c.connect(); kitaev@213: } kitaev@213: if (debug) { kitaev@213: System.out.printf("%d ranges, method:%s \n", ranges.size(), c.getRequestMethod()); kitaev@213: dumpResponseHeader(u, c); kitaev@213: } kitaev@213: InputStreamReader is = new InputStreamReader(c.getInputStream(), "US-ASCII"); kitaev@213: StreamTokenizer st = new StreamTokenizer(is); kitaev@213: st.ordinaryChars('0', '9'); kitaev@213: st.wordChars('0', '9'); kitaev@213: st.eolIsSignificant(true); kitaev@213: Iterator rangeItr = ranges.iterator(); kitaev@213: LinkedList currRangeList = null; kitaev@213: Range currRange = null; kitaev@213: boolean possiblyEmptyNextLine = true; kitaev@213: while (st.nextToken() != StreamTokenizer.TT_EOF) { kitaev@213: if (st.ttype == StreamTokenizer.TT_EOL) { kitaev@213: if (possiblyEmptyNextLine) { kitaev@213: // newline follows newline; kitaev@213: assert currRange == null; kitaev@213: assert currRangeList == null; kitaev@213: if (!rangeItr.hasNext()) { kitaev@213: throw new HgBadStateException(); kitaev@213: } kitaev@213: rv.put(rangeItr.next(), Collections.emptyList()); kitaev@213: } else { kitaev@213: if (currRange == null || currRangeList == null) { kitaev@213: throw new HgBadStateException(); kitaev@213: } kitaev@213: // indicate next range value is needed kitaev@213: currRange = null; kitaev@213: currRangeList = null; kitaev@213: possiblyEmptyNextLine = true; kitaev@213: } kitaev@213: } else { kitaev@213: possiblyEmptyNextLine = false; kitaev@213: if (currRange == null) { kitaev@213: if (!rangeItr.hasNext()) { kitaev@213: throw new HgBadStateException(); kitaev@213: } kitaev@213: currRange = rangeItr.next(); kitaev@213: currRangeList = new LinkedList(); kitaev@213: rv.put(currRange, currRangeList); kitaev@213: } kitaev@213: Nodeid nid = Nodeid.fromAscii(st.sval); kitaev@213: currRangeList.addLast(nid); kitaev@213: } kitaev@213: } kitaev@213: is.close(); kitaev@213: return rv; kitaev@213: } catch (MalformedURLException ex) { kitaev@213: throw new HgException(ex); kitaev@213: } catch (IOException ex) { kitaev@213: throw new HgException(ex); kitaev@213: } kitaev@213: } kitaev@213: kitaev@213: public List branches(List nodes) throws HgException { kitaev@213: StringBuilder sb = new StringBuilder(20 + nodes.size() * 41); kitaev@213: sb.append("nodes="); kitaev@213: for (Nodeid n : nodes) { kitaev@213: sb.append(n.toString()); kitaev@213: sb.append('+'); kitaev@213: } kitaev@213: if (sb.charAt(sb.length() - 1) == '+') { kitaev@213: // strip last space kitaev@213: sb.setLength(sb.length() - 1); kitaev@213: } kitaev@213: try { kitaev@213: URL u = new URL(url, url.getPath() + "?cmd=branches&" + sb.toString()); kitaev@213: HttpURLConnection c = setupConnection(u.openConnection()); kitaev@213: c.connect(); kitaev@213: if (debug) { kitaev@213: dumpResponseHeader(u, c); kitaev@213: } kitaev@213: InputStreamReader is = new InputStreamReader(c.getInputStream(), "US-ASCII"); kitaev@213: StreamTokenizer st = new StreamTokenizer(is); kitaev@213: st.ordinaryChars('0', '9'); kitaev@213: st.wordChars('0', '9'); kitaev@213: st.eolIsSignificant(false); kitaev@213: ArrayList parseResult = new ArrayList(nodes.size() * 4); kitaev@213: while (st.nextToken() != StreamTokenizer.TT_EOF) { kitaev@213: parseResult.add(Nodeid.fromAscii(st.sval)); kitaev@213: } kitaev@213: if (parseResult.size() != nodes.size() * 4) { kitaev@213: throw new HgException(String.format("Bad number of nodeids in result (shall be factor 4), expected %d, got %d", nodes.size()*4, parseResult.size())); kitaev@213: } kitaev@213: ArrayList rv = new ArrayList(nodes.size()); kitaev@213: for (int i = 0; i < nodes.size(); i++) { kitaev@213: RemoteBranch rb = new RemoteBranch(parseResult.get(i*4), parseResult.get(i*4 + 1), parseResult.get(i*4 + 2), parseResult.get(i*4 + 3)); kitaev@213: rv.add(rb); kitaev@213: } kitaev@213: return rv; kitaev@213: } catch (MalformedURLException ex) { kitaev@213: throw new HgException(ex); kitaev@213: } catch (IOException ex) { kitaev@213: throw new HgException(ex); kitaev@213: } kitaev@213: } kitaev@213: kitaev@213: /* kitaev@213: * XXX need to describe behavior when roots arg is empty; our RepositoryComparator code currently returns empty lists when kitaev@213: * no common elements found, which in turn means we need to query changes starting with NULL nodeid. kitaev@213: * kitaev@213: * WireProtocol wiki: roots = a list of the latest nodes on every service side changeset branch that both the client and server know about. kitaev@213: * kitaev@213: * Perhaps, shall be named 'changegroup' kitaev@213: kitaev@213: * Changegroup: kitaev@213: * http://mercurial.selenic.com/wiki/Merge kitaev@213: * http://mercurial.selenic.com/wiki/WireProtocol kitaev@213: * kitaev@213: * according to latter, bundleformat data is sent through zlib kitaev@213: * (there's no header like HG10?? with the server output, though, kitaev@213: * as one may expect according to http://mercurial.selenic.com/wiki/BundleFormat) kitaev@213: */ kitaev@213: public HgBundle getChanges(List roots) throws HgException { kitaev@213: List _roots = roots.isEmpty() ? Collections.singletonList(Nodeid.NULL) : roots; kitaev@213: StringBuilder sb = new StringBuilder(20 + _roots.size() * 41); kitaev@213: sb.append("roots="); kitaev@213: for (Nodeid n : _roots) { kitaev@213: sb.append(n.toString()); kitaev@213: sb.append('+'); kitaev@213: } kitaev@213: if (sb.charAt(sb.length() - 1) == '+') { kitaev@213: // strip last space kitaev@213: sb.setLength(sb.length() - 1); kitaev@213: } kitaev@213: try { kitaev@213: URL u = new URL(url, url.getPath() + "?cmd=changegroup&" + sb.toString()); kitaev@213: HttpURLConnection c = setupConnection(u.openConnection()); kitaev@213: c.connect(); kitaev@213: if (debug) { kitaev@213: dumpResponseHeader(u, c); kitaev@213: } kitaev@213: File tf = writeBundle(c.getInputStream(), false, "HG10GZ" /*didn't see any other that zip*/); kitaev@213: if (debug) { kitaev@213: System.out.printf("Wrote bundle %s for roots %s\n", tf, sb); kitaev@213: } kitaev@213: return getLookupHelper().loadBundle(tf); kitaev@213: } catch (MalformedURLException ex) { kitaev@213: throw new HgException(ex); kitaev@213: } catch (IOException ex) { kitaev@213: throw new HgException(ex); kitaev@213: } kitaev@213: } kitaev@213: kitaev@213: @Override kitaev@213: public String toString() { kitaev@213: return getClass().getSimpleName() + '[' + getLocation() + ']'; kitaev@213: } kitaev@213: kitaev@213: private HgLookup getLookupHelper() { kitaev@213: if (lookupHelper == null) { kitaev@213: lookupHelper = new HgLookup(); kitaev@213: } kitaev@213: return lookupHelper; kitaev@213: } kitaev@213: kitaev@213: private HttpURLConnection setupConnection(URLConnection urlConnection) { kitaev@213: urlConnection.setRequestProperty("User-Agent", "hg4j/0.5.0"); kitaev@213: urlConnection.addRequestProperty("Accept", "application/mercurial-0.1"); kitaev@213: if (authInfo != null) { kitaev@213: urlConnection.addRequestProperty("Authorization", "Basic " + authInfo); kitaev@213: } kitaev@213: if (sslContext != null) { kitaev@213: ((HttpsURLConnection) urlConnection).setSSLSocketFactory(sslContext.getSocketFactory()); kitaev@213: } kitaev@213: return (HttpURLConnection) urlConnection; kitaev@213: } kitaev@213: kitaev@213: private void dumpResponseHeader(URL u, HttpURLConnection c) { kitaev@213: System.out.printf("Query (%d bytes):%s\n", u.getQuery().length(), u.getQuery()); kitaev@213: System.out.println("Response headers:"); kitaev@213: final Map> headerFields = c.getHeaderFields(); kitaev@213: for (String s : headerFields.keySet()) { kitaev@213: System.out.printf("%s: %s\n", s, c.getHeaderField(s)); kitaev@213: } kitaev@213: } kitaev@213: kitaev@213: private static File writeBundle(InputStream is, boolean decompress, String header) throws IOException { kitaev@213: InputStream zipStream = decompress ? new InflaterInputStream(is) : is; kitaev@213: File tf = File.createTempFile("hg-bundle-", null); kitaev@213: FileOutputStream fos = new FileOutputStream(tf); kitaev@213: fos.write(header.getBytes()); kitaev@213: int r; kitaev@213: byte[] buf = new byte[8*1024]; kitaev@213: while ((r = zipStream.read(buf)) != -1) { kitaev@213: fos.write(buf, 0, r); kitaev@213: } kitaev@213: fos.close(); kitaev@213: zipStream.close(); kitaev@213: return tf; kitaev@213: } kitaev@213: kitaev@213: kitaev@213: public static final class Range { kitaev@213: /** kitaev@213: * Root of the range, earlier revision kitaev@213: */ kitaev@213: public final Nodeid start; kitaev@213: /** kitaev@213: * Head of the range, later revision. kitaev@213: */ kitaev@213: public final Nodeid end; kitaev@213: kitaev@213: /** kitaev@213: * @param from - root/base revision kitaev@213: * @param to - head/tip revision kitaev@213: */ kitaev@213: public Range(Nodeid from, Nodeid to) { kitaev@213: start = from; kitaev@213: end = to; kitaev@213: } kitaev@213: } kitaev@213: kitaev@213: public static final class RemoteBranch { kitaev@213: public final Nodeid head, root, p1, p2; kitaev@213: kitaev@213: public RemoteBranch(Nodeid h, Nodeid r, Nodeid parent1, Nodeid parent2) { kitaev@213: head = h; kitaev@213: root = r; kitaev@213: p1 = parent1; kitaev@213: p2 = parent2; kitaev@213: } kitaev@213: kitaev@213: @Override kitaev@213: public boolean equals(Object obj) { kitaev@213: if (this == obj) { kitaev@213: return true; kitaev@213: } kitaev@213: if (false == obj instanceof RemoteBranch) { kitaev@213: return false; kitaev@213: } kitaev@213: RemoteBranch o = (RemoteBranch) obj; kitaev@213: 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)); kitaev@213: } kitaev@213: } kitaev@213: }