/* * ==================================================================== * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. * ==================================================================== * * This software consists of voluntary contributions made by many * individuals on behalf of the Apache Software Foundation. For more * information on the Apache Software Foundation, please see * <http://www.apache.org/>. * */ package org.apache.ogt.http.conn.routing; import java.net.InetAddress; import java.util.HashSet; import java.util.Set; import org.apache.ogt.http.HttpHost; import org.apache.ogt.http.conn.routing.BasicRouteDirector; import org.apache.ogt.http.conn.routing.HttpRoute; import org.apache.ogt.http.conn.routing.HttpRouteDirector; import org.apache.ogt.http.conn.routing.RouteTracker; import org.apache.ogt.http.conn.routing.RouteInfo.LayerType; import org.apache.ogt.http.conn.routing.RouteInfo.TunnelType; import org.junit.Assert; import org.junit.Test; /** * Tests for {@link RouteTracker}. */ public class TestRouteTracker { // a selection of constants for generating routes public final static HttpHost TARGET1 = new HttpHost("target1.test.invalid"); public final static HttpHost TARGET2 = new HttpHost("target2.test.invalid", 8080); // It is not necessary to have extra targets for https. // The 'layered' and 'secure' flags are specified explicitly // for routes, they will not be determined from the scheme. public final static HttpHost PROXY1 = new HttpHost("proxy1.test.invalid"); public final static HttpHost PROXY2 = new HttpHost("proxy2.test.invalid", 1080); public final static HttpHost PROXY3 = new HttpHost("proxy3.test.invalid", 88); public final static InetAddress LOCAL41; public final static InetAddress LOCAL42; public final static InetAddress LOCAL61; public final static InetAddress LOCAL62; // need static initializer to deal with exceptions static { try { LOCAL41 = InetAddress.getByAddress(new byte[]{ 127, 0, 0, 1 }); LOCAL42 = InetAddress.getByAddress(new byte[]{ 127, 0, 0, 2 }); LOCAL61 = InetAddress.getByAddress(new byte[]{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 }); LOCAL62 = InetAddress.getByAddress(new byte[]{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2 }); } catch (Exception x) { throw new ExceptionInInitializerError(x); } } @Test public void testCstrTargetLocal() { RouteTracker rt = new RouteTracker(TARGET1, null); Assert.assertEquals("wrong target (target,null)", TARGET1, rt.getTargetHost()); Assert.assertEquals("wrong local address (target,null)", null, rt.getLocalAddress()); Assert.assertEquals("wrong hop count (target,null)", 0, rt.getHopCount()); Assert.assertEquals("wrong proxy (target,null)", null, rt.getProxyHost()); Assert.assertEquals("wrong route (target,null)", null, rt.toRoute()); checkCTLS(rt, false, false, false, false); rt = new RouteTracker(TARGET2, LOCAL61); Assert.assertEquals("wrong target (target,local)", TARGET2, rt.getTargetHost()); Assert.assertEquals("wrong local address (target,local)", LOCAL61, rt.getLocalAddress()); Assert.assertEquals("wrong hop count (target,local)", 0, rt.getHopCount()); Assert.assertEquals("wrong proxy (target,local)", null, rt.getProxyHost()); Assert.assertEquals("wrong route (target,local)", null, rt.toRoute()); checkCTLS(rt, false, false, false, false); rt = null; try { new RouteTracker(null, LOCAL41); Assert.fail("null target not detected"); } catch (IllegalArgumentException iax) { // expected } } @Test public void testCstrRoute() { HttpRoute r = new HttpRoute(TARGET1); RouteTracker rt = new RouteTracker(r); Assert.assertEquals("wrong target (r1)", TARGET1, rt.getTargetHost()); Assert.assertEquals("wrong local address (r1)", null, rt.getLocalAddress()); Assert.assertEquals("wrong hop count (r1)", 0, rt.getHopCount()); Assert.assertEquals("wrong proxy (r1)", null, rt.getProxyHost()); Assert.assertEquals("wrong route (r1)", null, rt.toRoute()); checkCTLS(rt, false, false, false, false); r = new HttpRoute(TARGET2, LOCAL61, true); rt = new RouteTracker(r); Assert.assertEquals("wrong target (r2)", TARGET2, rt.getTargetHost()); Assert.assertEquals("wrong local address (r2)", LOCAL61, rt.getLocalAddress()); Assert.assertEquals("wrong hop count (r2)", 0, rt.getHopCount()); Assert.assertEquals("wrong proxy (r2)", null, rt.getProxyHost()); Assert.assertEquals("wrong route (r2)", null, rt.toRoute()); checkCTLS(rt, false, false, false, false); r = new HttpRoute(TARGET1, LOCAL42, PROXY3, true); rt = new RouteTracker(r); Assert.assertEquals("wrong target (r3)", TARGET1, rt.getTargetHost()); Assert.assertEquals("wrong local address (r3)", LOCAL42, rt.getLocalAddress()); Assert.assertEquals("wrong hop count (r3)", 0, rt.getHopCount()); Assert.assertEquals("wrong proxy (r3)", null, rt.getProxyHost()); Assert.assertEquals("wrong route (r3)", null, rt.toRoute()); checkCTLS(rt, false, false, false, false); rt = null; try { new RouteTracker(null); Assert.fail("null route not detected"); } catch (NullPointerException npx) { // expected } } @Test public void testIllegalArgs() { RouteTracker rt = new RouteTracker(TARGET2, null); try { rt.connectProxy(null, true); Assert.fail("missing proxy argument not detected (connect/false)"); } catch (IllegalArgumentException iax) { // expected } try { rt.connectProxy(null, false); Assert.fail("missing proxy argument not detected (connect/true)"); } catch (IllegalArgumentException iax) { // expected } rt.connectProxy(PROXY1, false); try { rt.tunnelProxy(null, false); Assert.fail("missing proxy argument not detected (tunnel/false)"); } catch (IllegalArgumentException iax) { // expected } try { rt.tunnelProxy(null, true); Assert.fail("missing proxy argument not detected (tunnel/true)"); } catch (IllegalArgumentException iax) { // expected } try { rt.getHopTarget(-1); Assert.fail("negative hop index not detected"); } catch (IllegalArgumentException iax) { // expected } try { rt.getHopTarget(2); Assert.fail("excessive hop index not detected"); } catch (IllegalArgumentException iax) { // expected } } @Test public void testIllegalStates() { RouteTracker rt = new RouteTracker(TARGET1, null); try { rt.tunnelTarget(false); Assert.fail("unconnectedness not detected (tunnelTarget)"); } catch (IllegalStateException isx) { // expected } try { rt.tunnelProxy(PROXY1, false); Assert.fail("unconnectedness not detected (tunnelProxy)"); } catch (IllegalStateException isx) { // expected } try { rt.layerProtocol(true); Assert.fail("unconnectedness not detected (layerProtocol)"); } catch (IllegalStateException isx) { // expected } // connect directly rt.connectTarget(false); try { rt.connectTarget(false); Assert.fail("connectedness not detected (connectTarget)"); } catch (IllegalStateException isx) { // expected } try { rt.connectProxy(PROXY2, false); Assert.fail("connectedness not detected (connectProxy)"); } catch (IllegalStateException isx) { // expected } try { rt.tunnelTarget(false); Assert.fail("unproxiedness not detected (tunnelTarget)"); } catch (IllegalStateException isx) { // expected } try { rt.tunnelProxy(PROXY1, false); Assert.fail("unproxiedness not detected (tunnelProxy)"); } catch (IllegalStateException isx) { // expected } } @Test public void testDirectRoutes() { final HttpRouteDirector rd = new BasicRouteDirector(); HttpRoute r = new HttpRoute(TARGET1, LOCAL41, false); RouteTracker rt = new RouteTracker(r); boolean complete = checkVia(rt, r, rd, 2); Assert.assertTrue("incomplete route 1", complete); r = new HttpRoute(TARGET2, LOCAL62, true); rt = new RouteTracker(r); complete = checkVia(rt, r, rd, 2); Assert.assertTrue("incomplete route 2", complete); } @Test public void testProxyRoutes() { final HttpRouteDirector rd = new BasicRouteDirector(); HttpRoute r = new HttpRoute(TARGET2, null, PROXY1, false); RouteTracker rt = new RouteTracker(r); boolean complete = checkVia(rt, r, rd, 2); Assert.assertTrue("incomplete route 1", complete); // tunnelled, but neither secure nor layered r = new HttpRoute(TARGET1, LOCAL61, PROXY3, false, TunnelType.TUNNELLED, LayerType.PLAIN); rt = new RouteTracker(r); complete = checkVia(rt, r, rd, 3); Assert.assertTrue("incomplete route 2", complete); // tunnelled, layered, but not secure r = new HttpRoute(TARGET1, LOCAL61, PROXY3, false, TunnelType.TUNNELLED, LayerType.LAYERED); rt = new RouteTracker(r); complete = checkVia(rt, r, rd, 4); Assert.assertTrue("incomplete route 3", complete); // tunnelled, layered, secure r = new HttpRoute(TARGET1, LOCAL61, PROXY3, true); rt = new RouteTracker(r); complete = checkVia(rt, r, rd, 4); Assert.assertTrue("incomplete route 4", complete); } @Test public void testProxyChainRoutes() { final HttpRouteDirector rd = new BasicRouteDirector(); HttpHost[] proxies = { PROXY1, PROXY2 }; HttpRoute r = new HttpRoute(TARGET2, LOCAL42, proxies, false, TunnelType.PLAIN, LayerType.PLAIN); RouteTracker rt = new RouteTracker(r); boolean complete = checkVia(rt, r, rd, 3); Assert.assertTrue("incomplete route 1", complete); // tunnelled, but neither secure nor layered proxies = new HttpHost[]{ PROXY3, PROXY2 }; r = new HttpRoute(TARGET1, null, proxies, false, TunnelType.TUNNELLED, LayerType.PLAIN); rt = new RouteTracker(r); complete = checkVia(rt, r, rd, 4); Assert.assertTrue("incomplete route 2", complete); // tunnelled, layered, but not secure proxies = new HttpHost[]{ PROXY3, PROXY2, PROXY1 }; r = new HttpRoute(TARGET2, LOCAL61, proxies, false, TunnelType.TUNNELLED, LayerType.LAYERED); rt = new RouteTracker(r); complete = checkVia(rt, r, rd, 6); Assert.assertTrue("incomplete route 3", complete); // tunnelled, layered, secure proxies = new HttpHost[]{ PROXY1, PROXY3 }; r = new HttpRoute(TARGET1, LOCAL61, proxies, true, TunnelType.TUNNELLED, LayerType.LAYERED); rt = new RouteTracker(r); complete = checkVia(rt, r, rd, 5); Assert.assertTrue("incomplete route 4", complete); } @Test public void testEqualsHashcodeCloneToString() throws CloneNotSupportedException { RouteTracker rt0 = new RouteTracker(TARGET1, null); RouteTracker rt1 = new RouteTracker(TARGET2, null); RouteTracker rt2 = new RouteTracker(TARGET1, null); RouteTracker rt3 = new RouteTracker(TARGET1, null); RouteTracker rt4 = new RouteTracker(TARGET1, LOCAL41); RouteTracker rt6 = new RouteTracker(TARGET1, LOCAL62); Assert.assertFalse("rt0", rt0.equals(null)); Assert.assertTrue("rt0", rt0.equals(rt0)); Assert.assertFalse("rt0", rt0.equals(rt0.toString())); Assert.assertFalse("rt0 == rt4", rt0.equals(rt4)); Assert.assertFalse("rt0 == rt1", rt0.equals(rt1)); // Check host takes part in equals // Check that connection takes part in equals Assert.assertTrue("rt0 != rt2", rt0.equals(rt2)); rt2.connectTarget(false); Assert.assertFalse("rt0 == rt2", rt0.equals(rt2)); Assert.assertTrue("rt0 != rt3", rt0.equals(rt3)); rt3.connectTarget(true); Assert.assertFalse("rt0 == rt3", rt0.equals(rt3)); Assert.assertFalse("rt2 == rt3", rt2.equals(rt3)); // Test secure takes part // TODO needs tests for tunnel and layered Assert.assertFalse("rt4 == rt0", rt4.equals(rt0)); Assert.assertFalse("rt0 == rt6", rt0.equals(rt6)); Assert.assertFalse("rt6 == rt0", rt6.equals(rt0)); Assert.assertFalse("rt4 == rt6", rt4.equals(rt6)); Assert.assertFalse("rt6 == rt4", rt6.equals(rt4)); // it is likely but not guaranteed that the hashcodes are different Assert.assertFalse("rt0 == rt4 (hashcode)", rt0.hashCode() == rt4.hashCode()); Assert.assertFalse("rt0 == rt6 (hashcode)", rt0.hashCode() == rt6.hashCode()); Assert.assertFalse("rt6 == rt4 (hashcode)", rt6.hashCode() == rt4.hashCode()); Assert.assertEquals("rt0 (clone)", rt0, rt0.clone()); Assert.assertEquals("rt4 (clone)", rt4, rt4.clone()); Assert.assertEquals("rt6 (clone)", rt6, rt6.clone()); // we collect (clones of) the different tracked routes along the way // rt0 -> direct connection // rt1 -> via single proxy // rt2 -> via proxy chain Set<RouteTracker> hs = new HashSet<RouteTracker>(); // we also collect hashcodes for the different paths // since we can't guarantee what influence the HttpHost hashcodes have, // we keep separate sets here Set<Integer> hc0 = new HashSet<Integer>(); Set<Integer> hc4 = new HashSet<Integer>(); Set<Integer> hc6 = new HashSet<Integer>(); RouteTracker rt = null; Assert.assertTrue(hs.add(rt0)); Assert.assertTrue(hs.add(rt4)); Assert.assertTrue(hs.add(rt6)); Assert.assertTrue(hc0.add(Integer.valueOf(rt0.hashCode()))); Assert.assertTrue(hc4.add(Integer.valueOf(rt4.hashCode()))); Assert.assertTrue(hc6.add(Integer.valueOf(rt6.hashCode()))); rt = (RouteTracker) rt0.clone(); rt.connectTarget(false); Assert.assertTrue(hs.add(rt)); Assert.assertTrue(hc0.add(Integer.valueOf(rt.hashCode()))); rt = (RouteTracker) rt0.clone(); rt.connectTarget(true); Assert.assertTrue(hs.add(rt)); Assert.assertTrue(hc0.add(Integer.valueOf(rt.hashCode()))); // proxy (insecure) -> tunnel (insecure) -> layer (secure) rt = (RouteTracker) rt4.clone(); rt.connectProxy(PROXY1, false); Assert.assertTrue(hs.add((RouteTracker) rt.clone())); // this is not guaranteed to be unique... Assert.assertTrue(hc4.add(Integer.valueOf(rt.hashCode()))); rt.tunnelTarget(false); Assert.assertTrue(hs.add((RouteTracker) rt.clone())); Assert.assertTrue(hc4.add(Integer.valueOf(rt.hashCode()))); rt.layerProtocol(true); Assert.assertTrue(hs.add((RouteTracker) rt.clone())); Assert.assertTrue(hc4.add(Integer.valueOf(rt.hashCode()))); // proxy (secure) -> tunnel (secure) -> layer (insecure) rt = (RouteTracker) rt4.clone(); rt.connectProxy(PROXY1, true); Assert.assertTrue(hs.add((RouteTracker) rt.clone())); // this is not guaranteed to be unique... Assert.assertTrue(hc4.add(Integer.valueOf(rt.hashCode()))); rt.tunnelTarget(true); Assert.assertTrue(hs.add((RouteTracker) rt.clone())); Assert.assertTrue(hc4.add(Integer.valueOf(rt.hashCode()))); rt.layerProtocol(false); Assert.assertTrue(hs.add((RouteTracker) rt.clone())); Assert.assertTrue(hc4.add(Integer.valueOf(rt.hashCode()))); // PROXY1/i -> PROXY2/i -> tunnel/i -> layer/s rt = (RouteTracker) rt6.clone(); rt.connectProxy(PROXY1, false); Assert.assertTrue(hs.add((RouteTracker) rt.clone())); // this is not guaranteed to be unique... Assert.assertTrue(hc6.add(Integer.valueOf(rt.hashCode()))); rt.tunnelProxy(PROXY2, false); Assert.assertTrue(hs.add((RouteTracker) rt.clone())); // this is not guaranteed to be unique... Assert.assertTrue(hc6.add(Integer.valueOf(rt.hashCode()))); rt.tunnelTarget(false); Assert.assertTrue(hs.add((RouteTracker) rt.clone())); Assert.assertTrue(hc6.add(Integer.valueOf(rt.hashCode()))); rt.layerProtocol(true); Assert.assertTrue(hs.add((RouteTracker) rt.clone())); Assert.assertTrue(hc6.add(Integer.valueOf(rt.hashCode()))); // PROXY1/s -> PROXY2/s -> tunnel/s -> layer/i rt = (RouteTracker) rt6.clone(); rt.connectProxy(PROXY1, true); Assert.assertTrue(hs.add((RouteTracker) rt.clone())); // this is not guaranteed to be unique... Assert.assertTrue(hc6.add(Integer.valueOf(rt.hashCode()))); rt.tunnelProxy(PROXY2, true); Assert.assertTrue(hs.add((RouteTracker) rt.clone())); // this is not guaranteed to be unique... Assert.assertTrue(hc6.add(Integer.valueOf(rt.hashCode()))); rt.tunnelTarget(true); Assert.assertTrue(hs.add((RouteTracker) rt.clone())); Assert.assertTrue(hc6.add(Integer.valueOf(rt.hashCode()))); rt.layerProtocol(false); Assert.assertTrue(hs.add((RouteTracker) rt.clone())); Assert.assertTrue(hc6.add(Integer.valueOf(rt.hashCode()))); // PROXY2/i -> PROXY1/i -> tunnel/i -> layer/s rt = (RouteTracker) rt6.clone(); rt.connectProxy(PROXY2, false); Assert.assertTrue(hs.add((RouteTracker) rt.clone())); // this is not guaranteed to be unique... Assert.assertTrue(hc6.add(Integer.valueOf(rt.hashCode()))); rt.tunnelProxy(PROXY1, false); Assert.assertTrue(hs.add((RouteTracker) rt.clone())); // proxy chain sequence does not affect hashcode, so duplicate: // Assert.assertTrue(hc6.add(Integer.valueOf(rt.hashCode()))); rt.tunnelTarget(false); Assert.assertTrue(hs.add((RouteTracker) rt.clone())); // proxy chain sequence does not affect hashcode, so duplicate: // Assert.assertTrue(hc6.add(Integer.valueOf(rt.hashCode()))); rt.layerProtocol(true); Assert.assertTrue(hs.add((RouteTracker) rt.clone())); // proxy chain sequence does not affect hashcode, so duplicate: // Assert.assertTrue(hc6.add(Integer.valueOf(rt.hashCode()))); // check that all toString are OK and different Set<String> rtstrings = new HashSet<String>(); for (RouteTracker current: hs) { final String rts = checkToString(current); Assert.assertTrue("duplicate toString: " + rts, rtstrings.add(rts)); } } /** Helper to check the status of the four flags. */ public final static void checkCTLS(RouteTracker rt, boolean c, boolean t, boolean l, boolean s) { String rts = rt.toString(); Assert.assertEquals("wrong flag connected: " + rts, c, rt.isConnected()); Assert.assertEquals("wrong flag tunnelled: " + rts, t, rt.isTunnelled()); Assert.assertEquals("wrong enum tunnelled: " + rts, t ? TunnelType.TUNNELLED : TunnelType.PLAIN, rt.getTunnelType()); Assert.assertEquals("wrong flag layered: " + rts, l, rt.isLayered()); Assert.assertEquals("wrong enum layered: " + rts, l ? LayerType.LAYERED : LayerType.PLAIN, rt.getLayerType()); Assert.assertEquals("wrong flag secure: " + rts, s, rt.isSecure()); } /** * Helper to check tracking of a route. * This uses a {@link HttpRouteDirector} to fake establishing the route, * checking the intermediate steps. * * @param rt the tracker to check with * @param r the route to establish * @param rd the director to check with * @param steps the step count for this invocation * * @return <code>true</code> iff the route is complete */ public final static boolean checkVia(RouteTracker rt, HttpRoute r, HttpRouteDirector rd, int steps) { final String msg = r.toString() + " @ " + rt.toString(); boolean complete = false; while (!complete && (steps > 0)) { int action = rd.nextStep(r, rt.toRoute()); switch (action) { case HttpRouteDirector.COMPLETE: complete = true; Assert.assertEquals(r, rt.toRoute()); break; case HttpRouteDirector.CONNECT_TARGET: { final boolean sec = r.isSecure(); rt.connectTarget(sec); checkCTLS(rt, true, false, false, sec); Assert.assertEquals("wrong hop count "+msg, 1, rt.getHopCount()); Assert.assertEquals("wrong hop0 "+msg, r.getTargetHost(), rt.getHopTarget(0)); } break; case HttpRouteDirector.CONNECT_PROXY: { // we assume an insecure proxy connection final boolean sec = false; rt.connectProxy(r.getProxyHost(), sec); checkCTLS(rt, true, false, false, sec); Assert.assertEquals("wrong hop count "+msg, 2, rt.getHopCount()); Assert.assertEquals("wrong hop0 "+msg, r.getProxyHost(), rt.getHopTarget(0)); Assert.assertEquals("wrong hop1 "+msg, r.getTargetHost(), rt.getHopTarget(1)); } break; case HttpRouteDirector.TUNNEL_TARGET: { final int hops = rt.getHopCount(); // we assume an insecure tunnel final boolean sec = false; rt.tunnelTarget(sec); checkCTLS(rt, true, true, false, sec); Assert.assertEquals("wrong hop count "+msg, hops, rt.getHopCount()); Assert.assertEquals("wrong hop0 "+msg, r.getProxyHost(), rt.getHopTarget(0)); Assert.assertEquals("wrong hopN "+msg, r.getTargetHost(), rt.getHopTarget(hops-1)); } break; case HttpRouteDirector.TUNNEL_PROXY: { final int hops = rt.getHopCount(); // before tunnelling // we assume an insecure tunnel final boolean sec = false; final HttpHost pxy = r.getHopTarget(hops-1); rt.tunnelProxy(pxy, sec); // Since we're tunnelling to a proxy and not the target, // the 'tunelling' flag is false: no end-to-end tunnel. checkCTLS(rt, true, false, false, sec); Assert.assertEquals("wrong hop count "+msg, hops+1, rt.getHopCount()); Assert.assertEquals("wrong hop0 "+msg, r.getProxyHost(), rt.getHopTarget(0)); Assert.assertEquals("wrong hop"+hops+" "+msg, pxy, rt.getHopTarget(hops-1)); Assert.assertEquals("wrong hopN "+msg, r.getTargetHost(), rt.getHopTarget(hops)); } break; case HttpRouteDirector.LAYER_PROTOCOL: { final int hops = rt.getHopCount(); final boolean tun = rt.isTunnelled(); final boolean sec = r.isSecure(); rt.layerProtocol(sec); checkCTLS(rt, true, tun, true, sec); Assert.assertEquals("wrong hop count "+msg, hops, rt.getHopCount()); Assert.assertEquals("wrong proxy "+msg, r.getProxyHost(), rt.getProxyHost()); Assert.assertEquals("wrong target "+msg, r.getTargetHost(), rt.getTargetHost()); } break; // UNREACHABLE default: Assert.fail("unexpected action " + action + " from director, "+msg); break; } // switch steps--; } return complete; } // checkVia /** * Checks the output of <code>toString</code>. * * @param rt the tracker for which to check the output * * @return the result of <code>rt.toString()</code> */ public final static String checkToString(RouteTracker rt) { if (rt == null) return null; final String rts = rt.toString(); if (rt.getLocalAddress() != null) { final String las = rt.getLocalAddress().toString(); Assert.assertFalse("no local address in toString(): " + rts, rts.indexOf(las) < 0); } for (int i=0; i<rt.getHopCount(); i++) { final String hts = rt.getHopTarget(i).toString(); Assert.assertFalse("hop "+i+" ("+hts+") missing in toString(): " + rts, rts.indexOf(hts) < 0); } return rts; } }