Beispiele

Die folgenden Beispiele zeigen, wie angepasste Advisor implementiert werden können.

Standardadvisor

Dieser Beispielquellcode gleicht dem Standardadvisor HTTP Load Balancer. Er funktioniert wie folgt:

  1. Eine Sendeanforderung (Befehl "HEAD/HTTP") wird abgesetzt.
  2. Eine Antwort wird empfangen. Die Informationen werden nicht analysiert, aber die Antwort bewirkt, dass die Methode "getLoad" beendet wird.
  3. Die Methode "getLoad" gibt 0 für Erfolg oder -1 für einen Fehler zurück.

Dieser Advisor arbeitet im normalen Modus. Die Lastmessung basiert somit auf der abgelaufenen Zeit (in Millisekunden), die für das Öffnen des Sockets, das Senden, das Empfangen und das Schließen des Sockets benötigt wurde.

package CustomAdvisors;
import com.ibm.internet.lb.advisors.*;
public class ADV_sample extends ADV_Base implements ADV_MethodInterface {
  static final String  ADV_NAME              = "Sample";
  static final int     ADV_DEF_ADV_ON_PORT   = 80;
  static final int     ADV_DEF_INTERVAL      = 7;
  static final String  ADV_SEND_REQUEST      = 
    "HEAD / HTTP/1.0\r\nAccept: */*\r\nUser-Agent: " +
    "IBM_Load_Balancer_HTTP_Advisor\r\n\r\n";

//--------
// Konstruktor

  public ADV_sample() {
    super(ADV_NAME, "3.0.0.0-03.31.00", 
          ADV_DEF_ADV_ON_PORT, ADV_DEF_INTERVAL, "",
           false);
    super.setAdvisor( this );
  }

//--------
// ADV_AdvisorInitialize

  public void ADV_AdvisorInitialize() {
    return;                                  // normalerweise eine leere Routine
  }

//--------
// getLoad

  public int getLoad(int iConnectTime, ADV_Thread caller) {
	int iRc;
    int iLoad = ADV_HOST_INACCESSIBLE;       // initialisieren auf inaccessible (kein Zugriff)

    iRc = caller.send(ADV_SEND_REQUEST);     // HTTP-Anforderung an den
                                             // Server senden
    if (0 <= iRc) {                          // wenn das Senden erfolgreich war
      StringBuffer sbReceiveData = new StringBuffer("");   // Puffer für die
                                                           // Antwort zuordnen
      iRc = caller.receive(sbReceiveData);   // Ergebnis empfangen

      // Ergebnis analysieren, falls erforderlich

      if (0 <= iRc) {          // wenn der Empfang erfolgreich war
        iLoad = 0;             // 0 für Erfolg zurückgeben
      }                        // (Lastwert des Advisors wird im
    }                          //  normalen Modus vom Basiscode ignoriert)
	return iLoad;
  }
}

Advisor für Nebendatenströme

Das Beispiel veranschaulicht, wie der vom Advisorbasiscode geöffnete Standardsocket unterdrückt wird. Stattdessen öffnet dieser Advisor für die Abfrage eines Servers einen Java-Socket für Nebendatenströme. Diese Vorgehensweise kann für Server hilfreich sein, die für eine Advisorabfrage einen anderen Port als für den normalen Clientdatenverkehr verwenden.

In diesem Beispiel ist ein Server an Port 11999 empfangsbereit und gibt bei Abfrage einen Lastwert mit dem hexadezimalen Integer "4" zurück. Dieser Beispielcode wird im Ersetzungsmodus ausgeführt, d. h., der letzte Parameter des Advisorkonstruktors wird auf "true" gesetzt, und der Advisorbasiscode verwendet den zurückgegebenen Lastwert anstelle der abgelaufenen Zeit.

Beachten Sie den Aufruf von "supressBaseOpeningSocket()" in der Initialisierungsroutine. Eine Unterdrückung des Basissockets ist nicht erforderlich, wenn keine Daten gesendet werden. Sie können den Socket beispielsweise öffnen, um sicherzustellen, dass der Advisor eine Verbindung zum Server herstellen kann. Überprüfen Sie die Anforderungen Ihrer Anwendung sorgfältig, bevor Sie diese Auswahl treffen.

package CustomAdvisors;
import java.io.*;
import java.net.*;
import java.util.*;
import java.util.Date;
import com.ibm.internet.lb.advisors.*;
import com.ibm.internet.lb.common.*;
import com.ibm.internet.lb.server.SRV_ConfigServer;

public class ADV_sidea extends ADV_Base implements ADV_MethodInterface {
  static final String ADV_NAME = "sidea";
  static final int ADV_DEF_ADV_ON_PORT = 12345;
  static final int     ADV_DEF_INTERVAL      = 7;

  // Eine Bytefeldgruppe mit der Ladeanforderungsnachricht erstellen
  static final byte[] abHealth = {(byte)0x00, (byte)0x00, (byte)0x00,
                                  (byte)0x04};

  public ADV_sidea() {
    super(ADV_NAME, "3.0.0.0-03.31.00", ADV_DEF_ADV_ON_PORT,
          ADV_DEF_INTERVAL, "", 
          true);         // Parameter für Ersetzungsmodus ist true
    super.setAdvisor( this );
  }

//--------
// ADV_AdvisorInitialize

  public void ADV_AdvisorInitialize()
  { 
    suppressBaseOpeningSocket();   // Basiscode anweisen, den
                                   // Standardsocket nicht zu öffnen
    return;
  }

//--------
// getLoad

  public int getLoad(int iConnectTime, ADV_Thread caller) {
	int iRc;
	int iLoad = ADV_HOST_INACCESSIBLE;  // -1
    int iControlPort = 11999;   // Port, über den mit dem Server kommuniziert werden soll

    String sServer = caller.getCurrentServerId();   // Adresse des abzufragenden Servers
    try { 
      socket soServer = new Socket(sServer, iControlPort);  // Socket für
                                                            // Server öffnen
      DataInputStream disServer = new DataInputStream(
                                      soServer.getInputStream());
      DataOutputStream dosServer = new DataOutputStream(
                                       soServer.getOutputStream());
      
      int iRecvTimeout = 10000;  // Zeitlimit (in Millisekunden)
                                 // für den Empfang von Daten festlegen
      soServer.setSoTimeout(iRecvTimeout);

      dosServer.writeInt(4);     // Nachricht an den Server senden
      dosServer.flush();

      iLoad = disServer.readByte();   // Antwort vom Server empfangen

    } catch (exception e) {
      system.out.println("Caught exception " + e);
    }
    return iLoad;    // Vom Server gemeldete Last zurückgeben
  }
}

Advisor mit zwei Ports

Dieses Beispiel für einen angepassten Advisor veranschaulicht, wie Fehler für einen Port eines Servers basierend auf dem eigenen Status und dem Status eines anderen Serverdämons, der an einem anderen Port auf derselben Servermaschine ausgeführt wird, erkannt werden können. Wenn der HTTP-Dämon an Port 80 beispielsweise nicht mehr reagiert, möchten Sie möglicherweise auch keinen Datenverkehr mehr an den SSL-Dämon an Port 443 weiterleiten.

Dieser Advisor ist aggressiver als Standardadvisor, weil er jeden Server, der keine Antwort sendet, als funktionsunfähig betrachtet und deshalb als inaktiv markiert. Standardadvisor stufen nicht reagierende Server als sehr langsam ein. Dieser Advisor markiert einen Server für den HTTP-Port und den SSL-Port als inaktiv, wenn einer der Ports keine Antwort liefert.

Zur Verwendung dieses angepassten Advisors startet der Administrator zwei Instanzen des Advisors: eine für den HTTP-Port und eine für den SSL-Port. Der Advisor instanziiert zwei statische globale Hashtabellen, eine für HTTP und eine für SSL. Jeder Advisor versucht, mit seinem Serverdämon zu kommunizieren, und speichert die Ergebnisse dieses Ereignisses in seiner Hashtabelle. Der Wert, den jeder Advisor an die Basisadvisorklasse zurückgibt, ist abhängig von der Fähigkeit des Advisors zur Kommunikation mit seinem eigenen Serverdämon und der Fähigkeit des Partneradvisors zur Kommunikation mit dessen Dämon.

Die folgenden angepassten Methoden werden verwendet.

Die folgenden Fehlerbedingungen werden erkannt.

Das folgende Beispiel verknüpft Port 80 mit HTTP und Port 443 mit SSL, kann aber angepasst werden, um eine beliebige Portkombination festzulegen.

package CustomAdvisors;
import java.io.*;
import java.net.*;
import java.util.*;
import java.util.Date;
import com.ibm.internet.lb.advisors.*;
import com.ibm.internet.lb.common.*;
import com.ibm.internet.lb.manager.*;
import com.ibm.internet.lb.server.SRV_ConfigServer;

//--------
// Tabellenelement für die in diesem angepassten Advisor verwendeten Hashtabellen definieren

class ADV_nte implements Cloneable {
  private String  sCluster;
  private int     iPort;
  private String  sServer;
  private int     iLoad;
  private Date    dTimestamp;

//--------
// Konstruktor

  public ADV_nte(String sClusterIn, int iPortIn, String sServerIn,
                 int iLoadIn) {
    sCluster = sClusterIn;
    iPort = iPortIn;
    sServer = sServerIn;
    iLoad = iLoadIn;
    dTimestamp = new Date();
  }

//--------
// Prüfen, ob dieses Element aktuell oder abgelaufen ist
  public boolean isCurrent(ADV_twop oThis) {
    boolean bCurrent;
    int iLifetimeMs = 3 * 1000 * oThis.getInterval();   // 3 Advisorzyklen als
                                                        // Lebensdauer festlegen
    Date dNow = new Date();
    Date dExpires = new Date(dTimestamp.getTime() + iLifetimeMs);

    if (dNow.after(dExpires)) {
      bCurrent = false;
    } else {
      bCurrent = true;
    }
    return bCurrent;
  }

//--------
// Zugriffsmechanismen für Werte

  public int getLoadValue() { return iLoad; }
  
//--------
// Klonen (Beschädigung zwischen Threads vermeiden)

  public synchronized Object Clone() {
    try { 
      return super.clone();
    } catch (cloneNotSupportedException e) {
      return null;
    }
  }

}

//--------
// Angepassten Advisor definieren

public class ADV_twop extends ADV_Base 
   implements ADV_MethodInterface, ADV_AdvisorVersionInterface {
 
  static final int ADV_TWOP_PORT_HTTP = 80;
  static final int ADV_TWOP_PORT_SSL = 443;

  //--------
  // Tabellen für portspezifische Protokollinformationen definieren

  static HashTable htTwopHTTP = new Hashtable();
  static HashTable htTwopSSL = new Hashtable();

  static final String ADV_TWOP_NAME = "twop";
  static final int ADV_TWOP_DEF_ADV_ON_PORT = 80;
  static final int ADV_TWOP_DEF_INTERVAL = 7;
  static final String ADV_HTTP_REQUEST_STRING = 
    "HEAD / HTTP/1.0\r\nAccept: */*\r\nUser-Agent: " +
    "IBM_LB_Custom_Advisor\r\n\r\n";

  //--------
  // Bytefeldgruppe mit einer Hello-Nachricht des SSL-Clients erstellen
  
  public static final byte[] abClientHello = {
    (byte)0x80, (byte)0x1c,
    (byte)0x01,               // Client-Hello
    (byte)0x03, (byte)0x00,   // SSL-Version
    (byte)0x00, (byte)0x03,   // Länge der Verschlüsselungsspezifikation (Byte)
    (byte)0x00, (byte)0x00,   // Länge der Sitzungs-ID (Byte)
    (byte)0x00, (byte)0x10,   // Länge der Anforderungsdaten (Byte)
    (byte)0x00, (byte)0x00, (byte)0x03,   // Verschlüsselungsspezifikation
    (byte)0x1A, (byte)0xFC, (byte)0xE5, (byte)Ox20,  // Anforderungsdaten
    (byte)0xFD, (byte)0x3A, (byte)0x3C, (byte)0x18,  
    (byte)0xAB, (byte)0x67, (byte)0xB0, (byte)0x52, 
    (byte)0xB1, (byte)0x1D, (byte)0x55, (byte)0x44, (byte)0x0D, (byte)0x0A };

  //--------
  // Konstruktor

  public ADV_twop() {
    super(ADV_TWOP_NAME, VERSION, ADV_TWOP_DEF_ADV_ON_PORT, 
          ADV_TWOP_DEF_INTERVAL, "", 
          false);    // false = Load Balancer terminiert die Antwort
    setAdvisor ( this );
  }

  //--------
  // ADV_AdvisorInitialize

  public void ADV_AdvisorInitialize() {
    return;
  }

  //--------
  // Synchronisierte PUT- und GET-Zugriffsroutinen für die Hashtabellen

  synchronized ADV_nte getNte(Hashtable ht, String sName, String sHashKey) {
    ADV_nte nte = (ADV_nte)(ht.get(sHashKey));
    if (null != nte) {
      nte = (ADV_nte)nte.clone();
    }
    return nte;
  }
 synchronized void putNte(Hashtable ht, String sName, String sHashKey, 
                          ADV_nte nte) {
   ht.put(sHashKey,nte);
    return;
 }

  //--------
  // getLoadHTTP - HTTP-Last auf der Basis der Serverantwort bestimmen

  int getLoadHTTP(int iConnectTime, ADV_Thread caller) {
    int iLoad = ADV_HOST_INACCESSIBLE;

    int iRc = caller.send(ADV_HTTP_REQUEST_STRING);  // Anforderungsnachricht an
                                                     // Server senden
    if (0 <= iRc) {           // Hat die Anforderung einen Fehler zurückgegeben?
StringBuffer sbReceiveData = new StringBuffer("") // Puffer für die
                                                           // Antwort zuordnen
      iRc = caller.receive(sbReceiveData);    // Antwort vom Server abrufen

      if (0 <= iRc) {             // Hat receive einen Fehler zurückgegeben?
        if (0 < sbReceiveData.length()) {      // Sind Daten vorhanden?
          iLoad = SUCCESS;        // Abgerufene Daten ignorieren und
                                  // Erfolgscode zurückgeben
        }
      }
    }
	return iLoad;
  }

  //--------
  // getLoadSSL() - SSL-Last auf der Basis der Serverantwort bestimmen

  int getLoadSSL(int iConnectTime, ASV_Thread caller) {
    int iLoad = ADV_HOST_INACCESSIBLE;
	int iRc;

    CMNByteArrayWrapper cbawClientHello = new CMNByteArrayWrapper(
                                                  abClientHello);
    Socket socket = caller.getSocket();

    try {
        socket.getOutputStream().write(abClientHello);
        // receive (Empfang) durchführen
        socket.getInputStream().read();
   // Bei erfolgreichem Empfang 0 als Lastwert zurückgeben. Der Dateninhalt spielt keine
  // Rolle, und die Last wird vom Thread ADV_Thread berechnet.
			iLoad = 0;
    } catch (IOException e) {
        // Bei einem Fehler als Standardwert für iLoad verwenden.
    }
	return iLoad;
  }

  //--------
  // getLoad - Ergebnisse der HTTP- und SSL-Methoden zusammenführen

  public int getLoad(int iConnectTime, ADV_Thread caller) {
    int iLoadHTTP;
    int iLoadSSL;
    int iLoad;
	int iRc;

    String sCluster = caller.getCurrentClusterId();   // aktuelle Clusteradresse
    int iPort = getAdviseOnPort();
    String sServer = caller.getCurrentServerId();
    String sHashKey = sCluster = ":" + sServer;     // Schlüssel für Hashtabelle

    if (ADV_TWOP_PORT_HTTP == iPort) {              // HTTP-Server ausführen
      iLoadHTTP = getLoadHTTP(iConnectTime, caller);  // Last für HTTP abrufen

      ADV_nte nteHTTP = newADV_nte(sCluster, iPort, sServer, iLoadHTTP);
      putNte(htTwopHTTP, "HTTP", sHashKey, nteHTTP);  // HTTP-Lastinformationen
                                                      // speichern
      ADV_nte nteSSL = getNte(htTwopSSL, "SSL", sHashKey);  // SSL-Informationen
                                                            // abrufen
      if (null != nteSSL) { 
        if (true == nteSSL.isCurrent(this)) {         // Zeitmarke prüfen
          if (ADV_HOST_INACCESSIBLE != nteSSL.getLoadValue()) {    // Funktioniert
                                                                   // SSL?
            iLoad = iLoadHTTP;
          } else {    // SSL funktioniert nicht, deshalb HTTP-Server als inaktiv markieren
            iLoad= ADV_HOST_INACCESSIBLE;
          }
        } else {      // SSL-Informationen sind abgelaufen, deshalb
                      // HTTP-Server als inaktiv markieren
          iLoad = ADV_HOST_INACCESSIBLE; 
        }
      } else {        // Keine Lastinformationen zu SSL,
                      // Ergebnisse von getLoadHTTP() melden
        iLoad = iLoadHTTP;
      }
    }
    else if (ADV_TWOP_PORT_SSL == iPort) {           // SSL-Server ausführen
      iLoadSSL = getLoadSSL(iConnectTime, caller);   // Last für SSL abrufen

      ADV_nte nteSSL = new ADV_nte(sCluster, iPort, sServer, iLoadSSL);
      putNte(htTwopSSL, "SSL", sHashKey, nteSSL);   // SSL-Lastinformationen speichern

      ADV_nte nteHTTP = getNte(htTwopHTTP, "SSL", sHashKey);   // HTTP-Informationen
                                                               // abrufen
      if (null != nteHTTP) {
        if (true == nteHTTP.isCurrent(this)) {       // Zeitmarke prüfen
          if (ADV_HOST_INACCESSIBLE != nteHTTP.getLoadValue()) {  // Funktioniert
                                                                  // HTTP?
            iLoad = iLoadSSL; 
          } else {   // HTTP funktioniert nicht, deshalb SSL als inaktiv markieren
            iLoad = ADV_HOST_INACCESSIBLE; 
          }
        } else {     // Abgelaufene Informationen von HTTP, deshalb SSL als inaktiv markieren
          iLoad = ADV_HOST_INACCESSIBLE; 
        }
      } else {        // Keine Lastinformationen zu HTTP,
                      // Ergebnisse von getLoadSSL() melden
        iLoad = iLoadSSL;
      }
    }

  //--------
  // Fehlerbehandlungsroutine

    else { 
      iLoad = ADV_HOST_INACCESSIBLE;
    }
	return iLoad;
  }
}

Advisor für WebSphere Application Server

Ein Beispiel eines angepassten Advisors für WebSphere Application Server ist im Verzeichnis Installationspfad/servers/samples/CustomAdvisors/ enthalten. In diesem Dokument wird nicht der vollständige Code gezeigt.

Der vollständige Advisor ist nur geringfügig komplexer als das Beispiel. Er besitzt zusätzlich eine spezielle Parsing-Routine, die kompakter ist als das oben gezeigte StringTokenizer-Beispiel.

Der komplexere Teil des Beispielcodes befindet sich im Java-Servlet. Neben anderen Methoden enthält das Servlet zwei Methoden, die von der Servletspezifikation gefordert werden, "init()" und "service()", sowie eine Methode "run()", die von der Klasse "Java.lang.thread" gefordert wird.

Die relevanten Fragmente des Servletcodes werden im Folgenden gezeigt.

...

  public void init(ServletConfig config) throws ServletException {
    super.init(config);
    ...
    _checker = new Thread(this);
    _checker.start();
  }
  
  public void run() {
    setStatus(GOOD);

    while (true) {
      if (!getKeepRunning()) 
    return;
      setStatus(figureLoad());
      setLastUpdate(new java.util.Date());

      try {
        _checker.sleep(_interval * 1000);
      } catch (Exception ignore) { ; }
    }
  }

  public void service(HttpServletRequest req, HttpServletResponse res)
                      throws ServletException, IOException {

    ServletOutputStream out = null;
    try {
      out = res.getOutputStream();
    } catch (Exception e) { ... }
    ...
    res.setContentType("text/x-application-LBAdvisor");
    out.println(getStatusString());
    out.println(getLastUpdate().toString());
    out.flush();
    return;
  }

  ...

Zurückgegebene Daten der Advisor verwenden

Unabhängig davon, ob Sie einen Standardaufruf für einen vorhandenen Teil des Anwendungsservers verwenden oder einen neuen Codeabschnitt als serverseitiges Gegenstück zu Ihrem angepassten Advisor hinzufügen, möchten Sie eventuell die zurückgegebenen Lastwerte prüfen und das Serververhalten ändern. Die Java-Klasse "StringTokenizer" und die zugehörigen Methoden vereinfachen diese Überprüfung.

Der Inhalt eines typischen HTTP-Befehls könnte wie folgt aussehen: GET /index.html HTTP/1.0

Eine typische Antwort auf diesen Befehl könnte wie folgt aussehen:

HTTP/1.1 200 OK
Date: Mon, 20 November 2000 14:09:57 GMT
Server: Apache/1.3.12 (Linux and UNIX)
Content-Location: index.html.en
Vary: negotiate
TCN: choice
Last-Modified: Fri, 20 Oct 2000 15:58:35 GMT
ETag: "14f3e5-1a8-39f06bab;39f06a02"
Accept-Ranges: bytes
Content-Length: 424
Connection: close
Content-Type: text/html
Content-Language: en

<!DOCTYPE HTML PUBLIC "-//w3c//DTD HTML 3.2 Final//EN">
<HTML><HEAD><TITLE>Test Page</TITLE></HEAD>
<BODY><H1>Apache server</H1>
<HR>
<P><P>This Web server is running Apache 1.3.12.
<P><HR>
<P><IMG SRC="apache_pb.gif" ALT="">
</BODY></HTML>

Die Elemente von Interesse sind in der ersten Zeile enthalten. Dies ist insbesondere der HTTP-Rückkehrcode.

Die HTTP-Spezifikation klassifiziert Rückkehrcodes, die wie folgt zusammengefasst werden können:

Wenn Sie die Codes, die der Server zurückgeben kann, sehr genau kennen, muss Ihr Code nicht so detailliert wie in diesem Beispiel sein. Berücksichtigen Sie jedoch, dass sich eine Beschränkung der erkannten Rückkehrcodes auf die künftige Flexibilität Ihres Programms auswirken kann.

Das folgende Beispiel ist ein eigenständiges Java-Programm, das einen minimalen HTTP-Client enthält. In dem Beispiel wird ein einfacher, vielseitig einsetzbarer Parser für die Überprüfung der HTTP-Antwort aufgerufen.

import java.io.*;
import java.util.*;
import java.net.*;

public class ParseTest {
  static final int iPort = 80;
  static final String sServer = "www.ibm.com";
  static final String sQuery = "GET /index.html HTTP/1.0\r\n\r\n";
  static final String sHTTP10 = "HTTP/1.0";
  static final String sHTTP11 = "HTTP/1.1";

  public static void main(String[] Arg) {
    String sHTTPVersion = null;
    String sHTTPReturnCode = null;
    String sResponse = null;
    int iRc = 0;
    BufferedReader brIn = null;
    PrintWriter psOut = null;
    Socket soServer= null;
    StringBuffer sbText = new StringBuffer(40);

    try {
      soServer = new Socket(sServer, iPort);
      brIn = new BufferedReader(new InputStreamReader(
                                    soServer.getInputStream()));
      psOut = new PrintWriter(soServer.getOutputStream());
      psOut.println(sQuery);
      psOut.flush();
      sResponse = brIn.readLine();
      try {
        soServer.close();
      } catch (Exception sc) {;}
    }  catch (Exception swr) {;}
    
    StringTokenizer st = new StringTokenizer(sResponse, " ");
    if (true == st.hasMoreTokens()) {
      sHTTPVersion = st.nextToken();
      if (sHTTPVersion.equals(sHTTP110) || sHTTPVersion.equals(sHTTP11)) {
        System.out.println("HTTP Version: " + sHTTPVersion);
      } else {
        System.out.println("Invalid HTTP Version: " + sHTTPVersion);
      }
    } else {
      System.out.println("Nothing was returned");
    return;
    }

    if (true == st.hasMoreTokens()) {
      sHTTPReturnCode = st.nextToken();
      try {
        iRc = Integer.parseInt(sHTTPReturnCode);
      } catch (NumberFormatException ne) {;}

      switch (iRc) {
      case(200): 
        System.out.println("HTTP Response code: OK, " + iRc);
        break;
      case(400): case(401): case(402): case(403): case(404): 
        System.out.println("HTTP Response code: Client Error, " + iRc);
        break; 
      case(500): case(501): case(502): case(503):
        System.out.println("HTTP Response code: Server Error, " + iRc);
        break;
      default: 
        System.out.println("HTTP Response code: Unknown, " + iRc);
        break;
      }
    }

    if (true == st.hasMoreTokens()) {
      while (true == st.hasMoreTokens()) {
        sbText.append(st.nextToken());
        sbText.append("  ");
        }
      System.out.println("HTTP Response phrase: " + sbText.toString());
    }
  }
}