Package org.tmatesoft.hg.repo

Source Code of org.tmatesoft.hg.repo.HgRemoteRepository

/*
* Copyright (c) 2011-2013 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 static org.tmatesoft.hg.internal.remote.Connector.*;
import static org.tmatesoft.hg.util.Outcome.Kind.Failure;
import static org.tmatesoft.hg.util.Outcome.Kind.Success;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.StreamTokenizer;
import java.net.URI;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.tmatesoft.hg.auth.HgAuthFailedException;
import org.tmatesoft.hg.core.HgBadArgumentException;
import org.tmatesoft.hg.core.HgIOException;
import org.tmatesoft.hg.core.HgRemoteConnectionException;
import org.tmatesoft.hg.core.HgRepositoryNotFoundException;
import org.tmatesoft.hg.core.Nodeid;
import org.tmatesoft.hg.core.SessionContext;
import org.tmatesoft.hg.internal.BundleSerializer;
import org.tmatesoft.hg.internal.DataSerializer;
import org.tmatesoft.hg.internal.DataSerializer.OutputStreamSerializer;
import org.tmatesoft.hg.internal.EncodingHelper;
import org.tmatesoft.hg.internal.Experimental;
import org.tmatesoft.hg.internal.FileUtils;
import org.tmatesoft.hg.internal.Internals;
import org.tmatesoft.hg.internal.PropertyMarshal;
import org.tmatesoft.hg.internal.remote.Connector;
import org.tmatesoft.hg.internal.remote.RemoteConnectorDescriptor;
import org.tmatesoft.hg.util.Adaptable;
import org.tmatesoft.hg.util.LogFacility.Severity;
import org.tmatesoft.hg.util.Outcome;
import org.tmatesoft.hg.util.Pair;

/**
* WORK IN PROGRESS, DO NOT USE
*
* @see http://mercurial.selenic.com/wiki/WireProtocol
* @see http://mercurial.selenic.com/wiki/HttpCommandProtocol
*
* @author Artem Tikhomirov
* @author TMate Software Ltd.
*/
public class HgRemoteRepository implements SessionContext.Source {
 
  private final boolean debug;
  private HgLookup lookupHelper;
  private final SessionContext sessionContext;
  private Set<String> remoteCapabilities;
  private Connector remote;
 
  HgRemoteRepository(SessionContext ctx, RemoteDescriptor rd) throws HgBadArgumentException {
    RemoteConnectorDescriptor rcd = Adaptable.Factory.getAdapter(rd, RemoteConnectorDescriptor.class, null);
    if (rcd == null) {
      throw new IllegalArgumentException(String.format("Present implementation supports remote connections via %s only", Connector.class.getName()));
    }
    sessionContext = ctx;
    debug = new PropertyMarshal(ctx).getBoolean("hg4j.remote.debug", false);
    remote = rcd.createConnector();
    remote.init(rd /*sic! pass original*/, ctx, null);
  }
 
  public boolean isInvalid() throws HgRemoteConnectionException {
    initCapabilities();
    return remoteCapabilities.isEmpty();
  }

  /**
   * @return human-readable address of the server, without user credentials or any other security information
   */
  public String getLocation() {
    return remote.getServerLocation();
  }
 
  public SessionContext getSessionContext() {
    return sessionContext;
  }

  public List<Nodeid> heads() throws HgRemoteConnectionException {
    if (isInvalid()) {
      return Collections.emptyList();
    }
    try {
      remote.sessionBegin();
      InputStreamReader is = new InputStreamReader(remote.heads(), "US-ASCII");
      StreamTokenizer st = new StreamTokenizer(is);
      st.ordinaryChars('0', '9'); // wordChars performs |, hence need to 0 first
      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 (IOException ex) {
      throw new HgRemoteConnectionException("Communication failure", ex).setRemoteCommand(CMD_HEADS).setServerInfo(getLocation());
    } finally {
      remote.sessionEnd();
    }
  }
 
  public List<Nodeid> between(Nodeid tip, Nodeid base) throws HgRemoteConnectionException {
    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 HgRemoteConnectionException
   */
  public Map<Range, List<Nodeid>> between(Collection<Range> ranges) throws HgRemoteConnectionException {
    if (ranges.isEmpty() || isInvalid()) {
      return Collections.emptyMap();
    }
    LinkedHashMap<Range, List<Nodeid>> rv = new LinkedHashMap<HgRemoteRepository.Range, List<Nodeid>>(ranges.size() * 4 / 3);
    try {
      remote.sessionBegin();
      InputStreamReader is = new InputStreamReader(remote.between(ranges), "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 HgInvalidStateException("Internal error"); // TODO revisit-1.1
            }
            rv.put(rangeItr.next(), Collections.<Nodeid>emptyList());
          } else {
            if (currRange == null || currRangeList == null) {
              throw new HgInvalidStateException("Internal error"); // TODO revisit-1.1
            }
            // indicate next range value is needed
            currRange = null;
            currRangeList = null;
            possiblyEmptyNextLine = true;
          }
        } else {
          possiblyEmptyNextLine = false;
          if (currRange == null) {
            if (!rangeItr.hasNext()) {
              throw new HgInvalidStateException("Internal error"); // TODO revisit-1.1
            }
            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 (IOException ex) {
      throw new HgRemoteConnectionException("Communication failure", ex).setRemoteCommand(CMD_BETWEEN).setServerInfo(getLocation());
    } finally {
      remote.sessionEnd();
    }
  }

  public List<RemoteBranch> branches(List<Nodeid> nodes) throws HgRemoteConnectionException {
    if (isInvalid()) {
      return Collections.emptyList();
    }
    try {
      remote.sessionBegin();
      InputStreamReader is = new InputStreamReader(remote.branches(nodes), "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 HgRemoteConnectionException(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 (IOException ex) {
      throw new HgRemoteConnectionException("Communication failure", ex).setRemoteCommand(CMD_BRANCHES).setServerInfo(getLocation());
    } finally {
      remote.sessionEnd();
    }
  }

  /*
   * 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 HgRemoteConnectionException, HgRuntimeException {
    if (isInvalid()) {
      return null; // XXX valid retval???
    }
    List<Nodeid> _roots = roots.isEmpty() ? Collections.singletonList(Nodeid.NULL) : roots;
    try {
      remote.sessionBegin();
      File tf = writeBundle(remote.changegroup(_roots));
      if (debug) {
        System.out.printf("Wrote bundle %s for roots %s\n", tf, roots);
      }
      return getLookupHelper().loadBundle(tf);
    } catch (IOException ex) {
      throw new HgRemoteConnectionException("Communication failure", ex).setRemoteCommand(CMD_CHANGEGROUP).setServerInfo(getLocation());
    } catch (HgRepositoryNotFoundException ex) {
      throw new HgRemoteConnectionException("Communication failure", ex).setRemoteCommand(CMD_CHANGEGROUP).setServerInfo(getLocation());
    } finally {
      remote.sessionEnd();
    }
  }
 
  public void unbundle(HgBundle bundle, List<Nodeid> remoteHeads) throws HgRemoteConnectionException, HgRuntimeException {
    if (remoteHeads == null) {
      // TODO collect heads from bundle:
      // bundle.inspectChangelog(new HeadCollector(for each c : if collected has c.p1 or c.p2, remove them. Add c))
      // or get from remote server???
      throw Internals.notImplemented();
    }
    if (isInvalid()) {
      return;
    }
    DataSerializer.DataSource bundleData = BundleSerializer.newInstance(sessionContext, bundle);
    OutputStream os = null;
    try {
      remote.sessionBegin();
      os = remote.unbundle(bundleData.serializeLength(), remoteHeads);
      bundleData.serialize(new OutputStreamSerializer(os));
      os.flush();
      os.close();
      os = null;
    } catch (IOException ex) {
      throw new HgRemoteConnectionException("Communication failure", ex).setRemoteCommand("unbundle").setServerInfo(getLocation());
    } catch (HgIOException ex) {
      throw new HgRemoteConnectionException("Communication failure", ex).setRemoteCommand("unbundle").setServerInfo(getLocation());
    } finally {
      new FileUtils(sessionContext.getLog(), this).closeQuietly(os);
      remote.sessionEnd();
    }
  }

  public Bookmarks getBookmarks() throws HgRemoteConnectionException, HgRuntimeException {
    initCapabilities();
    if (!remoteCapabilities.contains(CMD_PUSHKEY)) { // (sic!) listkeys is available when pushkey in caps
      return new Bookmarks(Collections.<Pair<String, Nodeid>>emptyList());
    }
    final String actionName = "Get remote bookmarks";
    final List<Pair<String, String>> values = listkeys("bookmarks", actionName);
    ArrayList<Pair<String, Nodeid>> rv = new ArrayList<Pair<String, Nodeid>>();
    for (Pair<String, String> l : values) {
      if (l.second().length() != Nodeid.SIZE_ASCII) {
        sessionContext.getLog().dump(getClass(), Severity.Warn, "%s: bad nodeid '%s', ignored", actionName, l.second());
        continue;
      }
      Nodeid n = Nodeid.fromAscii(l.second());
      String bm = new String(l.first());
      rv.add(new Pair<String, Nodeid>(bm, n));
    }
    return new Bookmarks(rv);
  }

  public Outcome updateBookmark(String name, Nodeid oldRev, Nodeid newRev) throws HgRemoteConnectionException, HgRuntimeException {
    initCapabilities();
    if (!remoteCapabilities.contains(CMD_PUSHKEY)) {
      return new Outcome(Failure, "Server doesn't support pushkey protocol");
    }
    if (pushkey("Update remote bookmark", NS_BOOKMARKS, name, oldRev.toString(), newRev.toString())) {
      return new Outcome(Success, String.format("Bookmark %s updated to %s", name, newRev.shortNotation()));
    }
    return new Outcome(Failure, String.format("Bookmark update (%s: %s -> %s) failed", name, oldRev.shortNotation(), newRev.shortNotation()));
  }
 
  public Phases getPhases() throws HgRemoteConnectionException, HgRuntimeException {
    initCapabilities();
    if (!remoteCapabilities.contains(CMD_PUSHKEY)) {
      // old server defaults to publishing
      return new Phases(true, Collections.<Nodeid>emptyList());
    }
    final List<Pair<String, String>> values = listkeys(NS_PHASES, "Get remote phases");
    boolean publishing = false;
    ArrayList<Nodeid> draftRoots = new ArrayList<Nodeid>();
    for (Pair<String, String> l : values) {
      if ("publishing".equalsIgnoreCase(l.first())) {
        publishing = Boolean.parseBoolean(l.second());
        continue;
      }
      Nodeid root = Nodeid.fromAscii(l.first());
      int ph = Integer.parseInt(l.second());
      if (ph == HgPhase.Draft.mercurialOrdinal()) {
        draftRoots.add(root);
      } else {
        assert false;
        sessionContext.getLog().dump(getClass(), Severity.Error, "Unexpected phase value %d for revision %s", ph, root);
      }
    }
    return new Phases(publishing, draftRoots);
  }
 
  public Outcome updatePhase(HgPhase from, HgPhase to, Nodeid n) throws HgRemoteConnectionException, HgRuntimeException {
    initCapabilities();
    if (!remoteCapabilities.contains(CMD_PUSHKEY)) {
      return new Outcome(Failure, "Server doesn't support pushkey protocol");
    }
    if (pushkey("Update remote phases", NS_PHASES, n.toString(), String.valueOf(from.mercurialOrdinal()), String.valueOf(to.mercurialOrdinal()))) {
      return new Outcome(Success, String.format("Phase of %s updated to %s", n.shortNotation(), to.name()));
    }
    return new Outcome(Failure, String.format("Phase update (%s: %s -> %s) failed", n.shortNotation(), from.name(), to.name()));
  }

  @Override
  public String toString() {
    return getClass().getSimpleName() + '[' + getLocation() + ']';
  }
 
 
  private void initCapabilities() throws HgRemoteConnectionException {
    if (remoteCapabilities != null) {
      return;
    }
    try {
      remote.connect();
    } catch (HgAuthFailedException ex) {
      throw new HgRemoteConnectionException("Failed to authenticate", ex).setServerInfo(remote.getServerLocation());
    }
    try {
      remote.sessionBegin();
      String capsLine = remote.getCapabilities();
      String[] caps = capsLine.split("\\s");
      remoteCapabilities = new HashSet<String>(Arrays.asList(caps));
    } finally {
      remote.sessionEnd();
    }
  }

  private HgLookup getLookupHelper() {
    if (lookupHelper == null) {
      lookupHelper = new HgLookup(sessionContext);
    }
    return lookupHelper;
  }

  private List<Pair<String,String>> listkeys(String namespace, String actionName) throws HgRemoteConnectionException, HgRuntimeException {
    try {
      remote.sessionBegin();
      ArrayList<Pair<String, String>> rv = new ArrayList<Pair<String, String>>();
      InputStream response = remote.listkeys(namespace, actionName);
      // output of listkeys is encoded with UTF-8
      BufferedReader r = new BufferedReader(new InputStreamReader(response, EncodingHelper.getUTF8()));
      String l;
      while ((l = r.readLine()) != null) {
        int sep = l.indexOf('\t');
        if (sep == -1) {
          sessionContext.getLog().dump(getClass(), Severity.Warn, "%s: bad line '%s', ignored", actionName, l);
          continue;
        }
        rv.add(new Pair<String,String>(l.substring(0, sep), l.substring(sep+1)));
      }
      r.close();
      return rv;
    } catch (IOException ex) {
      throw new HgRemoteConnectionException("Communication failure", ex).setRemoteCommand(CMD_LISTKEYS).setServerInfo(getLocation());
    } finally {
      remote.sessionEnd();
    }
  }
 
  private boolean pushkey(String opName, String namespace, String key, String oldValue, String newValue) throws HgRemoteConnectionException, HgRuntimeException {
    try {
      remote.sessionBegin();
      final InputStream is = remote.pushkey(opName, namespace, key, oldValue, newValue);
      int rv = is.read();
      is.close();
      return rv == '1';
    } catch (IOException ex) {
      throw new HgRemoteConnectionException("Communication failure", ex).setRemoteCommand(CMD_PUSHKEY).setServerInfo(getLocation());
    } finally {
      remote.sessionEnd();
    }
  }
 
  private File writeBundle(InputStream is) throws IOException {
    File tf = File.createTempFile("hg4j-bundle-", null);
    new FileUtils(sessionContext.getLog(), this).write(is, tf);
    is.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;
    }
   
    /**
     * Append this range as pair of values 'end-start' to the supplied buffer and return the buffer.
     */
    public StringBuilder append(StringBuilder sb) {
      sb.append(end.toString());
      sb.append('-');
      sb.append(start.toString());
      return sb;
    }
  }

  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;
      // in fact, p1 and p2 are not supposed to be null, ever (at least for RemoteBranch created from server output)
      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));
    }
   
    @Override
    public int hashCode() {
      return head.hashCode() ^ root.hashCode();
    }
   
    @Override
    public String toString() {
      String none = String.valueOf(-1);
      String s1 = p1 == null || p1.isNull() ? none : p1.shortNotation();
      String s2 = p2 == null || p2.isNull() ? none : p2.shortNotation();
      return String.format("RemoteBranch[root: %s, head:%s, p1:%s, p2:%s]", root.shortNotation(), head.shortNotation(), s1, s2);
    }
  }

  public static final class Bookmarks implements Iterable<Pair<String, Nodeid>> {
    private final List<Pair<String, Nodeid>> bm;

    private Bookmarks(List<Pair<String, Nodeid>> bookmarks) {
      bm = bookmarks;
    }

    public Iterator<Pair<String, Nodeid>> iterator() {
      return bm.iterator();
    }
  }
 
  public static final class Phases {
    private final boolean pub;
    private final List<Nodeid> droots;
   
    private Phases(boolean publishing, List<Nodeid> draftRoots) {
      pub = publishing;
      droots = draftRoots;
    }
   
    /**
     * Non-publishing servers may (shall?) respond with a list of draft roots.
     * This method doesn't make sense when {@link #isPublishingServer()} is <code>true</code>
     *
     * @return list of draft roots on remote server
     */
    public List<Nodeid> draftRoots() {
      return droots;
    }

    /**
     * @return <code>true</code> if revisions on remote server shall be deemed published (either
     * old server w/o explicit setting, or a new one with <code>phases.publish == true</code>)
     */
    public boolean isPublishingServer() {
      return pub;
    }
  }

  /**
   * Session context  ({@link SessionContext#getRemoteDescriptor(URI)} gives descriptor of remote when asked.
   * Clients may supply own descriptors e.g. if need to pass extra information into Authenticator.
   * Present implementation of {@link HgRemoteRepository} will be happy with any {@link RemoteDescriptor} subclass
   * as long as it's {@link Adaptable adaptable} to {@link RemoteConnectorDescriptor}
   * @since 1.2
   */
  @Experimental(reason="Provisional API. Work in progress")
  public interface RemoteDescriptor {
    /**
     * @return remote location, never <code>null</code>
     */
    URI getURI();
  }
}
TOP

Related Classes of org.tmatesoft.hg.repo.HgRemoteRepository

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.