RefSpec.java

  1. /*
  2.  * Copyright (C) 2008, 2022 Shawn O. Pearce <spearce@spearce.org> and others
  3.  *
  4.  * This program and the accompanying materials are made available under the
  5.  * terms of the Eclipse Distribution License v. 1.0 which is available at
  6.  * https://www.eclipse.org/org/documents/edl-v10.php.
  7.  *
  8.  * SPDX-License-Identifier: BSD-3-Clause
  9.  */

  10. package org.eclipse.jgit.transport;

  11. import java.io.Serializable;
  12. import java.text.MessageFormat;
  13. import java.util.Objects;

  14. import org.eclipse.jgit.internal.JGitText;
  15. import org.eclipse.jgit.lib.Constants;
  16. import org.eclipse.jgit.lib.Ref;

  17. /**
  18.  * Describes how refs in one repository copy into another repository.
  19.  * <p>
  20.  * A ref specification provides matching support and limited rules to rewrite a
  21.  * reference in one repository to another reference in another repository.
  22.  */
  23. public class RefSpec implements Serializable {
  24.     private static final long serialVersionUID = 1L;

  25.     /**
  26.      * Suffix for wildcard ref spec component, that indicate matching all refs
  27.      * with specified prefix.
  28.      */
  29.     public static final String WILDCARD_SUFFIX = "/*"; //$NON-NLS-1$

  30.     /**
  31.      * Check whether provided string is a wildcard ref spec component.
  32.      *
  33.      * @param s
  34.      *            ref spec component - string to test. Can be null.
  35.      * @return true if provided string is a wildcard ref spec component.
  36.      */
  37.     public static boolean isWildcard(String s) {
  38.         return s != null && s.contains("*"); //$NON-NLS-1$
  39.     }

  40.     /** Does this specification ask for forced updated (rewind/reset)? */
  41.     private boolean force;

  42.     /** Is this specification actually a wildcard match? */
  43.     private boolean wildcard;

  44.     /** Is this the special ":" RefSpec? */
  45.     private boolean matching;

  46.     /** Is this a negative refspec. */
  47.     private boolean negative;

  48.     /**
  49.      * How strict to be about wildcards.
  50.      *
  51.      * @since 4.5
  52.      */
  53.     public enum WildcardMode {
  54.         /**
  55.          * Reject refspecs with an asterisk on the source side and not the
  56.          * destination side or vice versa. This is the mode used by FetchCommand
  57.          * and PushCommand to create a one-to-one mapping between source and
  58.          * destination refs.
  59.          */
  60.         REQUIRE_MATCH,
  61.         /**
  62.          * Allow refspecs with an asterisk on only one side. This can create a
  63.          * many-to-one mapping between source and destination refs, so
  64.          * expandFromSource and expandFromDestination are not usable in this
  65.          * mode.
  66.          */
  67.         ALLOW_MISMATCH
  68.     }

  69.     /** Whether a wildcard is allowed on one side but not the other. */
  70.     private WildcardMode allowMismatchedWildcards;

  71.     /** Name of the ref(s) we would copy from. */
  72.     private String srcName;

  73.     /** Name of the ref(s) we would copy into. */
  74.     private String dstName;

  75.     /**
  76.      * Construct an empty RefSpec.
  77.      * <p>
  78.      * A newly created empty RefSpec is not suitable for use in most
  79.      * applications, as at least one field must be set to match a source name.
  80.      */
  81.     public RefSpec() {
  82.         matching = false;
  83.         force = false;
  84.         wildcard = false;
  85.         srcName = Constants.HEAD;
  86.         dstName = null;
  87.         negative =false;
  88.         allowMismatchedWildcards = WildcardMode.REQUIRE_MATCH;
  89.     }

  90.     /**
  91.      * Parse a ref specification for use during transport operations.
  92.      * <p>
  93.      * {@link RefSpec}s can be regular or negative, regular RefSpecs indicate
  94.      * what to include in transport operations while negative RefSpecs indicate
  95.      * what to exclude in fetch.
  96.      * <p>
  97.      * Negative {@link RefSpec}s can't be force, must have only source or
  98.      * destination. Wildcard patterns are also supported in negative RefSpecs
  99.      * but they can not go with {@code WildcardMode.REQUIRE_MATCH} because they
  100.      * are natually one to many mappings.
  101.      *
  102.      * <p>
  103.      * Specifications are typically one of the following forms:
  104.      * <ul>
  105.      * <li><code>refs/heads/master</code></li>
  106.      * <li><code>refs/heads/master:refs/remotes/origin/master</code></li>
  107.      * <li><code>refs/heads/*:refs/remotes/origin/*</code></li>
  108.      * <li><code>+refs/heads/master</code></li>
  109.      * <li><code>+refs/heads/master:refs/remotes/origin/master</code></li>
  110.      * <li><code>+refs/heads/*:refs/remotes/origin/*</code></li>
  111.      * <li><code>+refs/pull/&#42;/head:refs/remotes/origin/pr/*</code></li>
  112.      * <li><code>:refs/heads/master</code></li>
  113.      * </ul>
  114.      *
  115.      * If the wildcard mode allows mismatches, then these ref specs are also
  116.      * valid:
  117.      * <ul>
  118.      * <li><code>refs/heads/*</code></li>
  119.      * <li><code>refs/heads/*:refs/heads/master</code></li>
  120.      * </ul>
  121.      *
  122.      * Negative specifications are usually like:
  123.      * <ul>
  124.      * <li><code>^:refs/heads/master</code></li>
  125.      * <li><code>^refs/heads/*</code></li>
  126.      * </ul>
  127.      *
  128.      * @param spec
  129.      *            string describing the specification.
  130.      * @param mode
  131.      *            whether to allow a wildcard on one side without a wildcard on
  132.      *            the other.
  133.      * @throws java.lang.IllegalArgumentException
  134.      *             the specification is invalid.
  135.      * @since 4.5
  136.      */
  137.     public RefSpec(String spec, WildcardMode mode) {
  138.         this.allowMismatchedWildcards = mode;
  139.         String s = spec;

  140.         if (s.startsWith("^+") || s.startsWith("+^")) { //$NON-NLS-1$ //$NON-NLS-2$
  141.             throw new IllegalArgumentException(
  142.                     JGitText.get().invalidNegativeAndForce);
  143.         }

  144.         if (s.startsWith("+")) { //$NON-NLS-1$
  145.             force = true;
  146.             s = s.substring(1);
  147.         }

  148.         if (s.startsWith("^")) { //$NON-NLS-1$
  149.             negative = true;
  150.             s = s.substring(1);
  151.         }

  152.         boolean matchPushSpec = false;
  153.         final int c = s.lastIndexOf(':');
  154.         if (c == 0) {
  155.             s = s.substring(1);
  156.             if (s.isEmpty()) {
  157.                 matchPushSpec = true;
  158.                 wildcard = true;
  159.                 srcName = Constants.R_HEADS + '*';
  160.                 dstName = srcName;
  161.             } else {
  162.                 if (isWildcard(s)) {
  163.                     wildcard = true;
  164.                     if (mode == WildcardMode.REQUIRE_MATCH) {
  165.                         throw new IllegalArgumentException(MessageFormat
  166.                                 .format(JGitText.get().invalidWildcards, spec));
  167.                     }
  168.                 }
  169.                 dstName = checkValid(s);
  170.             }
  171.         } else if (c > 0) {
  172.             String src = s.substring(0, c);
  173.             String dst = s.substring(c + 1);
  174.             if (isWildcard(src) && isWildcard(dst)) {
  175.                 // Both contain wildcard
  176.                 wildcard = true;
  177.             } else if (isWildcard(src) || isWildcard(dst)) {
  178.                 wildcard = true;
  179.                 if (mode == WildcardMode.REQUIRE_MATCH)
  180.                     throw new IllegalArgumentException(MessageFormat
  181.                             .format(JGitText.get().invalidWildcards, spec));
  182.             }
  183.             srcName = checkValid(src);
  184.             dstName = checkValid(dst);
  185.         } else {
  186.             if (isWildcard(s)) {
  187.                 if (mode == WildcardMode.REQUIRE_MATCH) {
  188.                     throw new IllegalArgumentException(MessageFormat
  189.                             .format(JGitText.get().invalidWildcards, spec));
  190.                 }
  191.                 wildcard = true;
  192.             }
  193.             srcName = checkValid(s);
  194.         }

  195.         // Negative refspecs must only have dstName or srcName.
  196.         if (isNegative()) {
  197.             if (isNullOrEmpty(srcName) && isNullOrEmpty(dstName)) {
  198.                 throw new IllegalArgumentException(MessageFormat
  199.                         .format(JGitText.get().invalidRefSpec, spec));
  200.             }
  201.             if (!isNullOrEmpty(srcName) && !isNullOrEmpty(dstName)) {
  202.                 throw new IllegalArgumentException(MessageFormat
  203.                         .format(JGitText.get().invalidRefSpec, spec));
  204.             }
  205.             if(wildcard && mode == WildcardMode.REQUIRE_MATCH) {
  206.                 throw new IllegalArgumentException(MessageFormat
  207.                         .format(JGitText.get().invalidRefSpec, spec));}
  208.         }
  209.         matching = matchPushSpec;
  210.     }

  211.     /**
  212.      * Parse a ref specification for use during transport operations.
  213.      * <p>
  214.      * Specifications are typically one of the following forms:
  215.      * <ul>
  216.      * <li><code>refs/heads/master</code></li>
  217.      * <li><code>refs/heads/master:refs/remotes/origin/master</code></li>
  218.      * <li><code>refs/heads/*:refs/remotes/origin/*</code></li>
  219.      * <li><code>+refs/heads/master</code></li>
  220.      * <li><code>+refs/heads/master:refs/remotes/origin/master</code></li>
  221.      * <li><code>+refs/heads/*:refs/remotes/origin/*</code></li>
  222.      * <li><code>+refs/pull/&#42;/head:refs/remotes/origin/pr/*</code></li>
  223.      * <li><code>:refs/heads/master</code></li>
  224.      * </ul>
  225.      *
  226.      * @param spec
  227.      *            string describing the specification.
  228.      * @throws java.lang.IllegalArgumentException
  229.      *             the specification is invalid.
  230.      */
  231.     public RefSpec(String spec) {
  232.         this(spec, spec.startsWith("^") ? WildcardMode.ALLOW_MISMATCH //$NON-NLS-1$
  233.                 : WildcardMode.REQUIRE_MATCH);
  234.     }

  235.     private RefSpec(RefSpec p) {
  236.         matching = false;
  237.         force = p.isForceUpdate();
  238.         wildcard = p.isWildcard();
  239.         negative = p.isNegative();
  240.         srcName = p.getSource();
  241.         dstName = p.getDestination();
  242.         allowMismatchedWildcards = p.allowMismatchedWildcards;
  243.     }

  244.     /**
  245.      * Tells whether this {@link RefSpec} is the special "matching" RefSpec ":"
  246.      * for pushing.
  247.      *
  248.      * @return whether this is a "matching" RefSpec
  249.      * @since 6.1
  250.      */
  251.     public boolean isMatching() {
  252.         return matching;
  253.     }

  254.     /**
  255.      * Check if this specification wants to forcefully update the destination.
  256.      *
  257.      * @return true if this specification asks for updates without merge tests.
  258.      */
  259.     public boolean isForceUpdate() {
  260.         return force;
  261.     }

  262.     /**
  263.      * Create a new RefSpec with a different force update setting.
  264.      *
  265.      * @param forceUpdate
  266.      *            new value for force update in the returned instance.
  267.      * @return a new RefSpec with force update as specified.
  268.      */
  269.     public RefSpec setForceUpdate(boolean forceUpdate) {
  270.         final RefSpec r = new RefSpec(this);
  271.         if (forceUpdate && isNegative()) {
  272.             throw new IllegalArgumentException(
  273.                     JGitText.get().invalidNegativeAndForce);
  274.         }
  275.         r.matching = matching;
  276.         r.force = forceUpdate;
  277.         return r;
  278.     }

  279.     /**
  280.      * Check if this specification is actually a wildcard pattern.
  281.      * <p>
  282.      * If this is a wildcard pattern then the source and destination names
  283.      * returned by {@link #getSource()} and {@link #getDestination()} will not
  284.      * be actual ref names, but instead will be patterns.
  285.      *
  286.      * @return true if this specification could match more than one ref.
  287.      */
  288.     public boolean isWildcard() {
  289.         return wildcard;
  290.     }

  291.     /**
  292.      * Check if this specification is a negative one.
  293.      *
  294.      * @return true if this specification is negative.
  295.      * @since 6.2
  296.      */
  297.     public boolean isNegative() {
  298.         return negative;
  299.     }

  300.     /**
  301.      * Get the source ref description.
  302.      * <p>
  303.      * During a fetch this is the name of the ref on the remote repository we
  304.      * are fetching from. During a push this is the name of the ref on the local
  305.      * repository we are pushing out from.
  306.      *
  307.      * @return name (or wildcard pattern) to match the source ref.
  308.      */
  309.     public String getSource() {
  310.         return srcName;
  311.     }

  312.     /**
  313.      * Create a new RefSpec with a different source name setting.
  314.      *
  315.      * @param source
  316.      *            new value for source in the returned instance.
  317.      * @return a new RefSpec with source as specified.
  318.      * @throws java.lang.IllegalStateException
  319.      *             There is already a destination configured, and the wildcard
  320.      *             status of the existing destination disagrees with the
  321.      *             wildcard status of the new source.
  322.      */
  323.     public RefSpec setSource(String source) {
  324.         final RefSpec r = new RefSpec(this);
  325.         r.srcName = checkValid(source);
  326.         if (isWildcard(r.srcName) && r.dstName == null)
  327.             throw new IllegalStateException(JGitText.get().destinationIsNotAWildcard);
  328.         if (isWildcard(r.srcName) != isWildcard(r.dstName))
  329.             throw new IllegalStateException(JGitText.get().sourceDestinationMustMatch);
  330.         return r;
  331.     }

  332.     /**
  333.      * Get the destination ref description.
  334.      * <p>
  335.      * During a fetch this is the local tracking branch that will be updated
  336.      * with the new ObjectId after fetching is complete. During a push this is
  337.      * the remote ref that will be updated by the remote's receive-pack process.
  338.      * <p>
  339.      * If null during a fetch no tracking branch should be updated and the
  340.      * ObjectId should be stored transiently in order to prepare a merge.
  341.      * <p>
  342.      * If null during a push, use {@link #getSource()} instead.
  343.      *
  344.      * @return name (or wildcard) pattern to match the destination ref.
  345.      */
  346.     public String getDestination() {
  347.         return dstName;
  348.     }

  349.     /**
  350.      * Create a new RefSpec with a different destination name setting.
  351.      *
  352.      * @param destination
  353.      *            new value for destination in the returned instance.
  354.      * @return a new RefSpec with destination as specified.
  355.      * @throws java.lang.IllegalStateException
  356.      *             There is already a source configured, and the wildcard status
  357.      *             of the existing source disagrees with the wildcard status of
  358.      *             the new destination.
  359.      */
  360.     public RefSpec setDestination(String destination) {
  361.         final RefSpec r = new RefSpec(this);
  362.         r.dstName = checkValid(destination);
  363.         if (isWildcard(r.dstName) && r.srcName == null)
  364.             throw new IllegalStateException(JGitText.get().sourceIsNotAWildcard);
  365.         if (isWildcard(r.srcName) != isWildcard(r.dstName))
  366.             throw new IllegalStateException(JGitText.get().sourceDestinationMustMatch);
  367.         return r;
  368.     }

  369.     /**
  370.      * Create a new RefSpec with a different source/destination name setting.
  371.      *
  372.      * @param source
  373.      *            new value for source in the returned instance.
  374.      * @param destination
  375.      *            new value for destination in the returned instance.
  376.      * @return a new RefSpec with destination as specified.
  377.      * @throws java.lang.IllegalArgumentException
  378.      *             The wildcard status of the new source disagrees with the
  379.      *             wildcard status of the new destination.
  380.      */
  381.     public RefSpec setSourceDestination(String source, String destination) {
  382.         if (isWildcard(source) != isWildcard(destination))
  383.             throw new IllegalStateException(JGitText.get().sourceDestinationMustMatch);
  384.         final RefSpec r = new RefSpec(this);
  385.         r.wildcard = isWildcard(source);
  386.         r.srcName = source;
  387.         r.dstName = destination;
  388.         return r;
  389.     }

  390.     /**
  391.      * Does this specification's source description match the ref name?
  392.      *
  393.      * @param r
  394.      *            ref name that should be tested.
  395.      * @return true if the names match; false otherwise.
  396.      */
  397.     public boolean matchSource(String r) {
  398.         return match(r, getSource());
  399.     }

  400.     /**
  401.      * Does this specification's source description match the ref?
  402.      *
  403.      * @param r
  404.      *            ref whose name should be tested.
  405.      * @return true if the names match; false otherwise.
  406.      */
  407.     public boolean matchSource(Ref r) {
  408.         return match(r.getName(), getSource());
  409.     }

  410.     /**
  411.      * Does this specification's destination description match the ref name?
  412.      *
  413.      * @param r
  414.      *            ref name that should be tested.
  415.      * @return true if the names match; false otherwise.
  416.      */
  417.     public boolean matchDestination(String r) {
  418.         return match(r, getDestination());
  419.     }

  420.     /**
  421.      * Does this specification's destination description match the ref?
  422.      *
  423.      * @param r
  424.      *            ref whose name should be tested.
  425.      * @return true if the names match; false otherwise.
  426.      */
  427.     public boolean matchDestination(Ref r) {
  428.         return match(r.getName(), getDestination());
  429.     }

  430.     /**
  431.      * Expand this specification to exactly match a ref name.
  432.      * <p>
  433.      * Callers must first verify the passed ref name matches this specification,
  434.      * otherwise expansion results may be unpredictable.
  435.      *
  436.      * @param r
  437.      *            a ref name that matched our source specification. Could be a
  438.      *            wildcard also.
  439.      * @return a new specification expanded from provided ref name. Result
  440.      *         specification is wildcard if and only if provided ref name is
  441.      *         wildcard.
  442.      * @throws java.lang.IllegalStateException
  443.      *             when the RefSpec was constructed with wildcard mode that
  444.      *             doesn't require matching wildcards.
  445.      */
  446.     public RefSpec expandFromSource(String r) {
  447.         if (allowMismatchedWildcards != WildcardMode.REQUIRE_MATCH) {
  448.             throw new IllegalStateException(
  449.                     JGitText.get().invalidExpandWildcard);
  450.         }
  451.         return isWildcard() ? new RefSpec(this).expandFromSourceImp(r) : this;
  452.     }

  453.     private RefSpec expandFromSourceImp(String name) {
  454.         final String psrc = srcName, pdst = dstName;
  455.         wildcard = false;
  456.         srcName = name;
  457.         dstName = expandWildcard(name, psrc, pdst);
  458.         return this;
  459.     }

  460.     private static boolean isNullOrEmpty(String refName) {
  461.         return refName == null || refName.isEmpty();
  462.     }

  463.     /**
  464.      * Expand this specification to exactly match a ref.
  465.      * <p>
  466.      * Callers must first verify the passed ref matches this specification,
  467.      * otherwise expansion results may be unpredictable.
  468.      *
  469.      * @param r
  470.      *            a ref that matched our source specification. Could be a
  471.      *            wildcard also.
  472.      * @return a new specification expanded from provided ref name. Result
  473.      *         specification is wildcard if and only if provided ref name is
  474.      *         wildcard.
  475.      * @throws java.lang.IllegalStateException
  476.      *             when the RefSpec was constructed with wildcard mode that
  477.      *             doesn't require matching wildcards.
  478.      */
  479.     public RefSpec expandFromSource(Ref r) {
  480.         return expandFromSource(r.getName());
  481.     }

  482.     /**
  483.      * Expand this specification to exactly match a ref name.
  484.      * <p>
  485.      * Callers must first verify the passed ref name matches this specification,
  486.      * otherwise expansion results may be unpredictable.
  487.      *
  488.      * @param r
  489.      *            a ref name that matched our destination specification. Could
  490.      *            be a wildcard also.
  491.      * @return a new specification expanded from provided ref name. Result
  492.      *         specification is wildcard if and only if provided ref name is
  493.      *         wildcard.
  494.      * @throws java.lang.IllegalStateException
  495.      *             when the RefSpec was constructed with wildcard mode that
  496.      *             doesn't require matching wildcards.
  497.      */
  498.     public RefSpec expandFromDestination(String r) {
  499.         if (allowMismatchedWildcards != WildcardMode.REQUIRE_MATCH) {
  500.             throw new IllegalStateException(
  501.                     JGitText.get().invalidExpandWildcard);
  502.         }
  503.         return isWildcard() ? new RefSpec(this).expandFromDstImp(r) : this;
  504.     }

  505.     private RefSpec expandFromDstImp(String name) {
  506.         final String psrc = srcName, pdst = dstName;
  507.         wildcard = false;
  508.         srcName = expandWildcard(name, pdst, psrc);
  509.         dstName = name;
  510.         return this;
  511.     }

  512.     /**
  513.      * Expand this specification to exactly match a ref.
  514.      * <p>
  515.      * Callers must first verify the passed ref matches this specification,
  516.      * otherwise expansion results may be unpredictable.
  517.      *
  518.      * @param r
  519.      *            a ref that matched our destination specification.
  520.      * @return a new specification expanded from provided ref name. Result
  521.      *         specification is wildcard if and only if provided ref name is
  522.      *         wildcard.
  523.      * @throws java.lang.IllegalStateException
  524.      *             when the RefSpec was constructed with wildcard mode that
  525.      *             doesn't require matching wildcards.
  526.      */
  527.     public RefSpec expandFromDestination(Ref r) {
  528.         return expandFromDestination(r.getName());
  529.     }

  530.     private boolean match(String name, String s) {
  531.         if (s == null)
  532.             return false;
  533.         if (isWildcard(s)) {
  534.             int wildcardIndex = s.indexOf('*');
  535.             String prefix = s.substring(0, wildcardIndex);
  536.             String suffix = s.substring(wildcardIndex + 1);
  537.             return name.length() > prefix.length() + suffix.length()
  538.                     && name.startsWith(prefix) && name.endsWith(suffix);
  539.         }
  540.         return name.equals(s);
  541.     }

  542.     private static String expandWildcard(String name, String patternA,
  543.             String patternB) {
  544.         int a = patternA.indexOf('*');
  545.         int trailingA = patternA.length() - (a + 1);
  546.         int b = patternB.indexOf('*');
  547.         String match = name.substring(a, name.length() - trailingA);
  548.         return patternB.substring(0, b) + match + patternB.substring(b + 1);
  549.     }

  550.     private static String checkValid(String spec) {
  551.         if (spec != null && !isValid(spec))
  552.             throw new IllegalArgumentException(MessageFormat.format(
  553.                     JGitText.get().invalidRefSpec, spec));
  554.         return spec;
  555.     }

  556.     private static boolean isValid(String s) {
  557.         if (s.startsWith("/")) //$NON-NLS-1$
  558.             return false;
  559.         if (s.contains("//")) //$NON-NLS-1$
  560.             return false;
  561.         if (s.endsWith("/")) //$NON-NLS-1$
  562.             return false;
  563.         int i = s.indexOf('*');
  564.         if (i != -1) {
  565.             if (s.indexOf('*', i + 1) > i)
  566.                 return false;
  567.         }
  568.         return true;
  569.     }

  570.     /** {@inheritDoc} */
  571.     @Override
  572.     public int hashCode() {
  573.         int hc = 0;
  574.         if (getSource() != null)
  575.             hc = hc * 31 + getSource().hashCode();
  576.         if (getDestination() != null)
  577.             hc = hc * 31 + getDestination().hashCode();
  578.         return hc;
  579.     }

  580.     /** {@inheritDoc} */
  581.     @Override
  582.     public boolean equals(Object obj) {
  583.         if (!(obj instanceof RefSpec))
  584.             return false;
  585.         final RefSpec b = (RefSpec) obj;
  586.         if (isForceUpdate() != b.isForceUpdate()) {
  587.             return false;
  588.         }
  589.         if(isNegative() != b.isNegative()) {
  590.             return false;
  591.         }
  592.         if (isMatching()) {
  593.             return b.isMatching();
  594.         } else if (b.isMatching()) {
  595.             return false;
  596.         }
  597.         return isWildcard() == b.isWildcard()
  598.                 && Objects.equals(getSource(), b.getSource())
  599.                 && Objects.equals(getDestination(), b.getDestination());
  600.     }

  601.     /** {@inheritDoc} */
  602.     @Override
  603.     public String toString() {
  604.         final StringBuilder r = new StringBuilder();
  605.         if (isForceUpdate()) {
  606.             r.append('+');
  607.         }
  608.         if(isNegative()) {
  609.             r.append('^');
  610.         }
  611.         if (isMatching()) {
  612.             r.append(':');
  613.         } else {
  614.             if (getSource() != null) {
  615.                 r.append(getSource());
  616.             }
  617.             if (getDestination() != null) {
  618.                 r.append(':');
  619.                 r.append(getDestination());
  620.             }
  621.         }
  622.         return r.toString();
  623.     }
  624. }