/**
   * Transmits a request for listening applications to highlight a given table row.
   *
   * @param tcModel topcat model of table to broadcast
   * @param lrow row index within tcModel
   * @param recipients array of plastic IDs for target applications; if null, broadcast will be to
   *     all
   * @return true iff message was broadcast successfully
   */
  public boolean highlightRow(TopcatModel tcModel, long lrow, URI[] recipients) throws IOException {

    /* Get the hub and ID. */
    register();
    PlasticHubListener hub = getHub();
    URI plasticId = getRegisteredId();
    int irow = Tables.checkedLongToInt(lrow);

    /* See if the table we're broadcasting the row for is any of the
     * tables we've previously broadcast.  If so, send the row using the
     * same ID. */
    boolean done = false;
    int sendRow = -1;
    String tableId = null;
    for (Iterator it = idMap_.entrySet().iterator(); !done && it.hasNext(); ) {
      Map.Entry entry = (Map.Entry) it.next();
      TableWithRows tr = (TableWithRows) entry.getValue();
      TopcatModel tcm = (TopcatModel) tr.tcModelRef_.get();
      if (tcm != null && tcm == tcModel) {
        int[] rowMap = tr.rowMap_;
        if (rowMap == null) {
          sendRow = irow;
        } else {
          for (int j = 0; j < rowMap.length; j++) {
            if (irow == rowMap[j]) {
              sendRow = j;
              break;
            }
          }
        }
        tableId = entry.getKey().toString();
        done = true;
      }
    }

    /* If that didn't result in any sends, try using the basic URL of
     * the table. */
    if (!done) {
      URL url = URLUtils.fixURL(tcModel.getDataModel().getBaseTable().getURL());
      if (url != null) {
        sendRow = irow;
        tableId = url.toString();
        done = true;
      }
    }

    /* Send the message if we've got the arguments. */
    if (done && sendRow >= 0) {
      List args =
          Arrays.asList(
              new Object[] {
                tableId, new Integer(sendRow),
              });
      if (recipients == null) {
        hub.requestAsynch(plasticId, MessageId.VOT_HIGHLIGHTOBJECT, args);
      } else {
        hub.requestToSubsetAsynch(
            plasticId, MessageId.VOT_HIGHLIGHTOBJECT, args, Arrays.asList(recipients));
      }
      return true;
    }

    /* Otherwise return failure status. */
    else {
      return false;
    }
  }
  /**
   * Sends a row subset to a specific list of PLASTIC listeners. It uses the <code>
   * ivo://votech.org/votable/showObjects</code> message.
   *
   * @param tcModel topcat model
   * @param rset row subset within tcModel
   * @param hub hub object
   * @param plasticId registration ID for this application
   * @param recipients listeners to receive it; null means do a broadcast
   */
  private void transmitSubset(
      TopcatModel tcModel,
      RowSubset rset,
      PlasticHubListener hub,
      URI plasticId,
      final URI[] recipients)
      throws IOException {

    /* See if the table we're broadcasting the set for is any of the
     * tables we've previously broadcast.  If so, send the rows using
     * the same ID. */
    boolean done = false;
    for (Iterator it = idMap_.entrySet().iterator(); it.hasNext(); ) {
      Map.Entry entry = (Map.Entry) it.next();
      String tableId = entry.getKey().toString();
      TableWithRows tr = (TableWithRows) entry.getValue();
      TopcatModel tcm = (TopcatModel) tr.tcModelRef_.get();
      if (tcm != null && tcm == tcModel) {
        List rowList = new ArrayList();

        /* Assemble a list of rows, possibly modulated by the
         * row order when the table was sent originally. */
        int[] rowMap = tr.rowMap_;
        if (rowMap == null) {
          int nrow = (int) Math.min((long) Integer.MAX_VALUE, tcModel.getDataModel().getRowCount());
          for (int i = 0; i < nrow; i++) {
            if (rset.isIncluded(i)) {
              rowList.add(new Integer(i));
            }
          }
        } else {
          int nrow = rowMap.length;
          for (int i = 0; i < nrow; i++) {
            if (rset.isIncluded(rowMap[i])) {
              rowList.add(new Integer(i));
            }
          }
        }

        /* Send the request. */
        List argList = Arrays.asList(new Object[] {tableId, rowList});
        if (recipients == null) {
          hub.requestAsynch(plasticId, MessageId.VOT_SHOWOBJECTS, argList);
        } else {
          hub.requestToSubsetAsynch(
              plasticId, MessageId.VOT_SHOWOBJECTS, argList, Arrays.asList(recipients));
        }
        done = true;
      }
    }

    /* If that didn't result in any sends, try using the basic URL of
     * the table. */
    if (!done) {
      URL url = URLUtils.fixURL(tcModel.getDataModel().getBaseTable().getURL());
      if (url != null) {
        List rowList = new ArrayList();
        int nrow = (int) Math.min((long) Integer.MAX_VALUE, tcModel.getDataModel().getRowCount());
        for (int i = 0; i < nrow; i++) {
          if (rset.isIncluded(i)) {
            rowList.add(new Integer(i));
          }
        }
        List argList = Arrays.asList(new Object[] {url.toString(), rowList});
        if (recipients == null) {
          hub.requestAsynch(plasticId, MessageId.VOT_SHOWOBJECTS, argList);
        } else {
          hub.requestToSubsetAsynch(
              plasticId, MessageId.VOT_SHOWOBJECTS, argList, Arrays.asList(recipients));
        }
      }
    }
  }