diff hg4j/src/main/java/org/tmatesoft/hg/repo/HgRemoteRepository.java @ 213:6ec4af642ba8 gradle

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