Пример #1
0
  /* (non-Javadoc)
   * @see org.eclipse.paho.client.mqttv3.IMqttAsyncClient#disconnect(long, java.lang.Object, org.eclipse.paho.client.mqttv3.IMqttActionListener)
   */
  public IMqttToken disconnect(
      long quiesceTimeout, Object userContext, IMqttActionListener callback) throws MqttException {
    final String methodName = "disconnect";
    // @TRACE 104=> quiesceTimeout={0} userContext={1} callback={2}
    log.fine(
        CLASS_NAME,
        methodName,
        "104",
        new Object[] {new Long(quiesceTimeout), userContext, callback});

    MqttToken token = new MqttToken(getClientId());
    token.setActionCallback(callback);
    token.setUserContext(userContext);

    MqttDisconnect disconnect = new MqttDisconnect();
    try {
      comms.disconnect(disconnect, quiesceTimeout, token);
    } catch (MqttException ex) {
      // @TRACE 105=< exception
      log.fine(CLASS_NAME, methodName, "105", null, ex);
      throw ex;
    }
    // @TRACE 108=<
    log.fine(CLASS_NAME, methodName, "108");

    return token;
  }
Пример #2
0
  protected NetworkModule[] createNetworkModules(String address, MqttConnectOptions options)
      throws MqttException, MqttSecurityException {
    final String methodName = "createNetworkModules";
    // @TRACE 116=URI={0}
    log.fine(CLASS_NAME, methodName, "116", new Object[] {address});

    NetworkModule[] networkModules = null;
    String[] serverURIs = options.getServerURIs();
    String[] array = null;
    if (serverURIs == null) {
      array = new String[] {address};
    } else if (serverURIs.length == 0) {
      array = new String[] {address};
    } else {
      array = serverURIs;
    }

    networkModules = new NetworkModule[array.length];
    for (int i = 0; i < array.length; i++) {
      networkModules[i] = createNetworkModule(array[i], options);
    }

    log.fine(CLASS_NAME, methodName, "108");
    return networkModules;
  }
Пример #3
0
 /* (non-Javadoc)
  * @see org.eclipse.paho.client.mqttv3.IMqttAsyncClient#close()
  */
 public void close() throws MqttException {
   final String methodName = "close";
   // @TRACE 113=<
   log.fine(CLASS_NAME, methodName, "113");
   comms.close();
   // @TRACE 114=>
   log.fine(CLASS_NAME, methodName, "114");
 }
Пример #4
0
  /* (non-Javadoc)
   * Check and send a ping if needed.
   * <p>By default, client sends PingReq to server to keep the connection to
   * server. For some platforms which cannot use this mechanism, such as Android,
   * developer needs to handle the ping request manually with this method.
   * </p>
   *
   * @throws MqttException for other errors encountered while publishing the message.
   */
  public IMqttToken checkPing(Object userContext, IMqttActionListener callback)
      throws MqttException {
    final String methodName = "ping";
    MqttToken token;
    // @TRACE 117=>
    log.fine(CLASS_NAME, methodName, "117");

    token = comms.checkForActivity();
    // @TRACE 118=<
    log.fine(CLASS_NAME, methodName, "118");

    return token;
  }
Пример #5
0
 private void stopReconnectCycle() {
   String methodName = "stopReconnectCycle";
   // @Trace 504=Stop reconnect timer for client: {0}
   log.fine(CLASS_NAME, methodName, "504", new Object[] {this.clientId});
   reconnectTimer.cancel();
   reconnectDelay = 1000; // Reset Delay Timer
 }
Пример #6
0
 private void startReconnectCycle() {
   String methodName = "startReconnectCycle";
   // @Trace 503=Start reconnect timer for client: {0}, delay: {1}
   log.fine(CLASS_NAME, methodName, "503", new Object[] {this.clientId, new Long(reconnectDelay)});
   reconnectTimer = new Timer("MQTT Reconnect: " + clientId);
   reconnectTimer.schedule(new ReconnectTask(), reconnectDelay);
 }
Пример #7
0
  /**
   * Attempts to reconnect the client to the server. If successful it will make sure that there are
   * no further reconnects scheduled. However if the connect fails, the delay will double up to 128
   * seconds and will re-schedule the reconnect for after the delay.
   *
   * <p>Any thrown exceptions are logged but not acted upon as it is assumed that they are being
   * thrown due to the server being offline and so reconnect attempts will continue.
   */
  private void attemptReconnect() {
    final String methodName = "attemptReconnect";
    // @Trace 500=Attempting to reconnect client: {0}
    log.fine(CLASS_NAME, methodName, "500", new Object[] {this.clientId});
    try {
      connect(
          this.connOpts,
          this.userContext,
          new IMqttActionListener() {

            public void onSuccess(IMqttToken asyncActionToken) {
              // @Trace 501=Automatic Reconnect Successful: {0}
              log.fine(
                  CLASS_NAME,
                  methodName,
                  "501",
                  new Object[] {asyncActionToken.getClient().getClientId()});
              comms.setRestingState(false);
              stopReconnectCycle();
            }

            public void onFailure(IMqttToken asyncActionToken, Throwable exception) {
              // @Trace 502=Automatic Reconnect failed, rescheduling: {0}
              log.fine(
                  CLASS_NAME,
                  methodName,
                  "502",
                  new Object[] {asyncActionToken.getClient().getClientId()});
              if (reconnectDelay < 128000) {
                reconnectDelay = reconnectDelay * 2;
              }
              rescheduleReconnectCycle(reconnectDelay);
            }
          });
    } catch (MqttSecurityException ex) {
      // @TRACE 804=exception
      log.fine(CLASS_NAME, methodName, "804", null, ex);
    } catch (MqttException ex) {
      // @TRACE 804=exception
      log.fine(CLASS_NAME, methodName, "804", null, ex);
    }
  }
Пример #8
0
  /**
   * Create an MqttAsyncClient that is used to communicate with an MQTT server.
   *
   * <p>The address of a server can be specified on the constructor. Alternatively a list containing
   * one or more servers can be specified using the {@link
   * MqttConnectOptions#setServerURIs(String[]) setServerURIs} method on MqttConnectOptions.
   *
   * <p>The <code>serverURI</code> parameter is typically used with the the <code>clientId</code>
   * parameter to form a key. The key is used to store and reference messages while they are being
   * delivered. Hence the serverURI specified on the constructor must still be specified even if a
   * list of servers is specified on an MqttConnectOptions object. The serverURI on the constructor
   * must remain the same across restarts of the client for delivery of messages to be maintained
   * from a given client to a given server or set of servers.
   *
   * <p>The address of the server to connect to is specified as a URI. Two types of connection are
   * supported <code>tcp://</code> for a TCP connection and <code>ssl://</code> for a TCP connection
   * secured by SSL/TLS. For example:
   *
   * <ul>
   *   <li><code>tcp://localhost:1883</code>
   *   <li><code>ssl://localhost:8883</code>
   * </ul>
   *
   * If the port is not specified, it will default to 1883 for <code>tcp://</code>" URIs, and 8883
   * for <code>ssl://</code> URIs.
   *
   * <p>A client identifier <code>clientId</code> must be specified and be less that 65535
   * characters. It must be unique across all clients connecting to the same server. The clientId is
   * used by the server to store data related to the client, hence it is important that the clientId
   * remain the same when connecting to a server if durable subscriptions or reliable messaging are
   * required.
   *
   * <p>A convenience method is provided to generate a random client id that should satisfy this
   * criteria - {@link #generateClientId()}. As the client identifier is used by the server to
   * identify a client when it reconnects, the client must use the same identifier between
   * connections if durable subscriptions or reliable delivery of messages is required.
   *
   * <p>In Java SE, SSL can be configured in one of several ways, which the client will use in the
   * following order:
   *
   * <ul>
   *   <li><strong>Supplying an <code>SSLSocketFactory</code></strong> - applications can use {@link
   *       MqttConnectOptions#setSocketFactory(SocketFactory)} to supply a factory with the
   *       appropriate SSL settings.
   *   <li><strong>SSL Properties</strong> - applications can supply SSL settings as a simple Java
   *       Properties using {@link MqttConnectOptions#setSSLProperties(Properties)}.
   *   <li><strong>Use JVM settings</strong> - There are a number of standard Java system properties
   *       that can be used to configure key and trust stores.
   * </ul>
   *
   * <p>In Java ME, the platform settings are used for SSL connections.
   *
   * <p>A persistence mechanism is used to enable reliable messaging. For messages sent at qualities
   * of service (QoS) 1 or 2 to be reliably delivered, messages must be stored (on both the client
   * and server) until the delivery of the message is complete. If messages are not safely stored
   * when being delivered then a failure in the client or server can result in lost messages. A
   * pluggable persistence mechanism is supported via the {@link MqttClientPersistence} interface.
   * An implementer of this interface that safely stores messages must be specified in order for
   * delivery of messages to be reliable. In addition {@link
   * MqttConnectOptions#setCleanSession(boolean)} must be set to false. In the event that only QoS 0
   * messages are sent or received or cleanSession is set to true then a safe store is not needed.
   *
   * <p>An implementation of file-based persistence is provided in class {@link
   * MqttDefaultFilePersistence} which will work in all Java SE based systems. If no persistence is
   * needed, the persistence parameter can be explicitly set to <code>null</code>.
   *
   * @param serverURI the address of the server to connect to, specified as a URI. Can be overridden
   *     using {@link MqttConnectOptions#setServerURIs(String[])}
   * @param clientId a client identifier that is unique on the server being connected to
   * @param persistence the persistence class to use to store in-flight message. If null then the
   *     default persistence mechanism is used
   * @throws IllegalArgumentException if the URI does not start with "tcp://", "ssl://" or
   *     "local://"
   * @throws IllegalArgumentException if the clientId is null or is greater than 65535 characters in
   *     length
   * @throws MqttException if any other problem was encountered
   */
  public MqttAsyncClient(
      String serverURI,
      String clientId,
      MqttClientPersistence persistence,
      MqttPingSender pingSender)
      throws MqttException {
    final String methodName = "MqttAsyncClient";

    log.setResourceName(clientId);

    if (clientId == null) { // Support empty client Id, 3.1.1 standard
      throw new IllegalArgumentException("Null clientId");
    }
    // Count characters, surrogate pairs count as one character.
    int clientIdLength = 0;
    for (int i = 0; i < clientId.length() - 1; i++) {
      if (Character_isHighSurrogate(clientId.charAt(i))) i++;
      clientIdLength++;
    }
    if (clientIdLength > 65535) {
      throw new IllegalArgumentException("ClientId longer than 65535 characters");
    }

    MqttConnectOptions.validateURI(serverURI);

    this.serverURI = serverURI;
    this.clientId = clientId;

    this.persistence = persistence;
    if (this.persistence == null) {
      this.persistence = new MemoryPersistence();
    }

    // @TRACE 101=<init> ClientID={0} ServerURI={1} PersistenceType={2}
    log.fine(CLASS_NAME, methodName, "101", new Object[] {clientId, serverURI, persistence});

    this.persistence.open(clientId, serverURI);
    this.comms = new ClientComms(this, this.persistence, pingSender);
    this.persistence.close();
    this.topics = new Hashtable();
  }
Пример #9
0
  /* (non-Javadoc)
   * @see org.eclipse.paho.client.mqttv3.IMqttAsyncClient#subscribe(java.lang.String[], int[], java.lang.Object, org.eclipse.paho.client.mqttv3.IMqttActionListener)
   */
  public IMqttToken subscribe(
      String[] topicFilters, int[] qos, Object userContext, IMqttActionListener callback)
      throws MqttException {
    final String methodName = "subscribe";

    if (topicFilters.length != qos.length) {
      throw new IllegalArgumentException();
    }

    // remove any message handlers for individual topics
    for (int i = 0; i < topicFilters.length; ++i) {
      this.comms.removeMessageListener(topicFilters[i]);
    }

    String subs = "";
    for (int i = 0; i < topicFilters.length; i++) {
      if (i > 0) {
        subs += ", ";
      }
      subs += "topic=" + topicFilters[i] + " qos=" + qos[i];

      // Check if the topic filter is valid before subscribing
      MqttTopic.validate(topicFilters[i], true /*allow wildcards*/);
    }
    // @TRACE 106=Subscribe topicFilter={0} userContext={1} callback={2}
    log.fine(CLASS_NAME, methodName, "106", new Object[] {subs, userContext, callback});

    MqttToken token = new MqttToken(getClientId());
    token.setActionCallback(callback);
    token.setUserContext(userContext);
    token.internalTok.setTopics(topicFilters);

    MqttSubscribe register = new MqttSubscribe(topicFilters, qos);

    comms.sendNoWait(register, token);
    // @TRACE 109=<
    log.fine(CLASS_NAME, methodName, "109");

    return token;
  }
Пример #10
0
  /* (non-Javadoc)
   * @see org.eclipse.paho.client.mqttv3.IMqttAsyncClient#unsubscribe(java.lang.String[], java.lang.Object, org.eclipse.paho.client.mqttv3.IMqttActionListener)
   */
  public IMqttToken unsubscribe(
      String[] topicFilters, Object userContext, IMqttActionListener callback)
      throws MqttException {
    final String methodName = "unsubscribe";
    String subs = "";
    for (int i = 0; i < topicFilters.length; i++) {
      if (i > 0) {
        subs += ", ";
      }
      subs += topicFilters[i];

      // Check if the topic filter is valid before unsubscribing
      // Although we already checked when subscribing, but invalid
      // topic filter is meanless for unsubscribing, just prohibit it
      // to reduce unnecessary control packet send to broker.
      MqttTopic.validate(topicFilters[i], true /*allow wildcards*/);
    }

    // @TRACE 107=Unsubscribe topic={0} userContext={1} callback={2}
    log.fine(CLASS_NAME, methodName, "107", new Object[] {subs, userContext, callback});

    // remove message handlers from the list for this client
    for (int i = 0; i < topicFilters.length; ++i) {
      this.comms.removeMessageListener(topicFilters[i]);
    }

    MqttToken token = new MqttToken(getClientId());
    token.setActionCallback(callback);
    token.setUserContext(userContext);
    token.internalTok.setTopics(topicFilters);

    MqttUnsubscribe unregister = new MqttUnsubscribe(topicFilters);

    comms.sendNoWait(unregister, token);
    // @TRACE 110=<
    log.fine(CLASS_NAME, methodName, "110");

    return token;
  }
Пример #11
0
  /* (non-Javadoc)
   * @see org.eclipse.paho.client.mqttv3.IMqttAsyncClient#publish(java.lang.String, org.eclipse.paho.client.mqttv3.MqttMessage, java.lang.Object, org.eclipse.paho.client.mqttv3.IMqttActionListener)
   */
  public IMqttDeliveryToken publish(
      String topic, MqttMessage message, Object userContext, IMqttActionListener callback)
      throws MqttException, MqttPersistenceException {
    final String methodName = "publish";
    // @TRACE 111=< topic={0} message={1}userContext={1} callback={2}
    log.fine(CLASS_NAME, methodName, "111", new Object[] {topic, userContext, callback});

    // Checks if a topic is valid when publishing a message.
    MqttTopic.validate(topic, false /*wildcards NOT allowed*/);

    MqttDeliveryToken token = new MqttDeliveryToken(getClientId());
    token.setActionCallback(callback);
    token.setUserContext(userContext);
    token.setMessage(message);
    token.internalTok.setTopics(new String[] {topic});

    MqttPublish pubMsg = new MqttPublish(topic, message);
    comms.sendNoWait(pubMsg, token);

    // @TRACE 112=<
    log.fine(CLASS_NAME, methodName, "112");

    return token;
  }
Пример #12
0
  /**
   * User triggered attempt to reconnect
   *
   * @throws MqttException
   */
  public void reconnect() throws MqttException {
    final String methodName = "reconnect";
    // @Trace 500=Attempting to reconnect client: {0}
    log.fine(CLASS_NAME, methodName, "500", new Object[] {this.clientId});
    // Some checks to make sure that we're not attempting to reconnect an already connected client
    if (comms.isConnected()) {
      throw ExceptionHelper.createMqttException(MqttException.REASON_CODE_CLIENT_CONNECTED);
    }
    if (comms.isConnecting()) {
      throw new MqttException(MqttException.REASON_CODE_CONNECT_IN_PROGRESS);
    }
    if (comms.isDisconnecting()) {
      throw new MqttException(MqttException.REASON_CODE_CLIENT_DISCONNECTING);
    }
    if (comms.isClosed()) {
      throw new MqttException(MqttException.REASON_CODE_CLIENT_CLOSED);
    }
    // We don't want to spam the server
    stopReconnectCycle();

    attemptReconnect();
  }
Пример #13
0
  /* (non-Javadoc)
   * @see org.eclipse.paho.client.mqttv3.IMqttAsyncClient#connect(org.eclipse.paho.client.mqttv3.MqttConnectOptions, java.lang.Object, org.eclipse.paho.client.mqttv3.IMqttActionListener)
   */
  public IMqttToken connect(
      MqttConnectOptions options, Object userContext, IMqttActionListener callback)
      throws MqttException, MqttSecurityException {
    final String methodName = "connect";
    if (comms.isConnected()) {
      throw ExceptionHelper.createMqttException(MqttException.REASON_CODE_CLIENT_CONNECTED);
    }
    if (comms.isConnecting()) {
      throw new MqttException(MqttException.REASON_CODE_CONNECT_IN_PROGRESS);
    }
    if (comms.isDisconnecting()) {
      throw new MqttException(MqttException.REASON_CODE_CLIENT_DISCONNECTING);
    }
    if (comms.isClosed()) {
      throw new MqttException(MqttException.REASON_CODE_CLIENT_CLOSED);
    }

    this.connOpts = options;
    this.userContext = userContext;
    final boolean automaticReconnect = options.isAutomaticReconnect();

    // @TRACE 103=cleanSession={0} connectionTimeout={1} TimekeepAlive={2} userName={3} password={4}
    // will={5} userContext={6} callback={7}
    log.fine(
        CLASS_NAME,
        methodName,
        "103",
        new Object[] {
          Boolean.valueOf(options.isCleanSession()),
          new Integer(options.getConnectionTimeout()),
          new Integer(options.getKeepAliveInterval()),
          options.getUserName(),
          ((null == options.getPassword()) ? "[null]" : "[notnull]"),
          ((null == options.getWillMessage()) ? "[null]" : "[notnull]"),
          userContext,
          callback
        });
    comms.setNetworkModules(createNetworkModules(serverURI, options));
    comms.setReconnectCallback(
        new MqttCallbackExtended() {

          public void messageArrived(String topic, MqttMessage message) throws Exception {}

          public void deliveryComplete(IMqttDeliveryToken token) {}

          public void connectComplete(boolean reconnect, String serverURI) {}

          public void connectionLost(Throwable cause) {
            if (automaticReconnect) {
              // Automatic reconnect is set so make sure comms is in resting state
              comms.setRestingState(true);
              reconnecting = true;
              startReconnectCycle();
            }
          }
        });

    // Insert our own callback to iterate through the URIs till the connect succeeds
    MqttToken userToken = new MqttToken(getClientId());
    ConnectActionListener connectActionListener =
        new ConnectActionListener(
            this, persistence, comms, options, userToken, userContext, callback, reconnecting);
    userToken.setActionCallback(connectActionListener);
    userToken.setUserContext(this);

    // If we are using the MqttCallbackExtended, set it on the connectActionListener
    if (this.mqttCallback instanceof MqttCallbackExtended) {
      connectActionListener.setMqttCallbackExtended((MqttCallbackExtended) this.mqttCallback);
    }

    comms.setNetworkModuleIndex(0);
    connectActionListener.connect();

    return userToken;
  }
Пример #14
0
  /**
   * Factory method to create the correct network module, based on the supplied address URI.
   *
   * @param address the URI for the server.
   * @param Connect options
   * @return a network module appropriate to the specified address.
   */
  private NetworkModule createNetworkModule(String address, MqttConnectOptions options)
      throws MqttException, MqttSecurityException {
    final String methodName = "createNetworkModule";
    // @TRACE 115=URI={0}
    log.fine(CLASS_NAME, methodName, "115", new Object[] {address});

    NetworkModule netModule;
    String shortAddress;
    String host;
    int port;
    SocketFactory factory = options.getSocketFactory();

    int serverURIType = MqttConnectOptions.validateURI(address);

    switch (serverURIType) {
      case MqttConnectOptions.URI_TYPE_TCP:
        shortAddress = address.substring(6);
        host = getHostName(shortAddress);
        port = getPort(shortAddress, 1883);
        if (factory == null) {
          factory = SocketFactory.getDefault();
        } else if (factory instanceof SSLSocketFactory) {
          throw ExceptionHelper.createMqttException(
              MqttException.REASON_CODE_SOCKET_FACTORY_MISMATCH);
        }
        netModule = new TCPNetworkModule(factory, host, port, clientId);
        ((TCPNetworkModule) netModule).setConnectTimeout(options.getConnectionTimeout());
        break;
      case MqttConnectOptions.URI_TYPE_SSL:
        shortAddress = address.substring(6);
        host = getHostName(shortAddress);
        port = getPort(shortAddress, 8883);
        SSLSocketFactoryFactory factoryFactory = null;
        if (factory == null) {
          //				try {
          factoryFactory = new SSLSocketFactoryFactory();
          Properties sslClientProps = options.getSSLProperties();
          if (null != sslClientProps) factoryFactory.initialize(sslClientProps, null);
          factory = factoryFactory.createSocketFactory(null);
          //				}
          //				catch (MqttDirectException ex) {
          //					throw ExceptionHelper.createMqttException(ex.getCause());
          //				}
        } else if ((factory instanceof SSLSocketFactory) == false) {
          throw ExceptionHelper.createMqttException(
              MqttException.REASON_CODE_SOCKET_FACTORY_MISMATCH);
        }

        // Create the network module...
        netModule = new SSLNetworkModule((SSLSocketFactory) factory, host, port, clientId);
        ((SSLNetworkModule) netModule).setSSLhandshakeTimeout(options.getConnectionTimeout());
        // Ciphers suites need to be set, if they are available
        if (factoryFactory != null) {
          String[] enabledCiphers = factoryFactory.getEnabledCipherSuites(null);
          if (enabledCiphers != null) {
            ((SSLNetworkModule) netModule).setEnabledCiphers(enabledCiphers);
          }
        }
        break;
      case MqttConnectOptions.URI_TYPE_WS:
        shortAddress = address.substring(5);
        host = getHostName(shortAddress);
        port = getPort(shortAddress, 80);
        if (factory == null) {
          factory = SocketFactory.getDefault();
        } else if (factory instanceof SSLSocketFactory) {
          throw ExceptionHelper.createMqttException(
              MqttException.REASON_CODE_SOCKET_FACTORY_MISMATCH);
        }
        netModule = new WebSocketNetworkModule(factory, address, host, port, clientId);
        ((WebSocketNetworkModule) netModule).setConnectTimeout(options.getConnectionTimeout());
        break;
      case MqttConnectOptions.URI_TYPE_WSS:
        shortAddress = address.substring(6);
        host = getHostName(shortAddress);
        port = getPort(shortAddress, 443);
        SSLSocketFactoryFactory wSSFactoryFactory = null;
        if (factory == null) {
          wSSFactoryFactory = new SSLSocketFactoryFactory();
          Properties sslClientProps = options.getSSLProperties();
          if (null != sslClientProps) wSSFactoryFactory.initialize(sslClientProps, null);
          factory = wSSFactoryFactory.createSocketFactory(null);

        } else if ((factory instanceof SSLSocketFactory) == false) {
          throw ExceptionHelper.createMqttException(
              MqttException.REASON_CODE_SOCKET_FACTORY_MISMATCH);
        }

        // Create the network module...
        netModule =
            new WebSocketSecureNetworkModule(
                (SSLSocketFactory) factory, address, host, port, clientId);
        ((WebSocketSecureNetworkModule) netModule)
            .setSSLhandshakeTimeout(options.getConnectionTimeout());
        // Ciphers suites need to be set, if they are available
        if (wSSFactoryFactory != null) {
          String[] enabledCiphers = wSSFactoryFactory.getEnabledCipherSuites(null);
          if (enabledCiphers != null) {
            ((SSLNetworkModule) netModule).setEnabledCiphers(enabledCiphers);
          }
        }
        break;
      case MqttConnectOptions.URI_TYPE_LOCAL:
        netModule = new LocalNetworkModule(address.substring(8));
        break;
      default:
        // This shouldn't happen, as long as validateURI() has been called.
        netModule = null;
    }
    return netModule;
  }
Пример #15
0
 private void rescheduleReconnectCycle(int delay) {
   String methodName = "rescheduleReconnectCycle";
   // @Trace 505=Rescheduling reconnect timer for client: {0}, delay: {1}
   log.fine(CLASS_NAME, methodName, "505", new Object[] {this.clientId, new Long(reconnectDelay)});
   reconnectTimer.schedule(new ReconnectTask(), reconnectDelay);
 }