예제 #1
0
// Constructor
ContactSet::ContactSet(ResourceCached* resource,
                       UtlString& uri) :
   mResource(resource),
   mUri(uri)
{
   OsSysLog::add(FAC_RLS, PRI_DEBUG,
                 "ContactSet:: this = %p, resource = %p, mUri = '%s'",
                 this, mResource, mUri.data());

   // Set up the subscriptions.
   // Until we have any information from our SUBSCRIBE for "reg" events,
   // there will be one subscription to mUri.
   updateSubscriptions();

   // Start the subscription.
   UtlBoolean ret;
   UtlString mUriNameAddr = "<" + mUri + ">";
   OsSysLog::add(FAC_RLS, PRI_DEBUG,
                 "SubscriptionSet:: mUri = '%s', mUriNameAddr = '%s'",
                 mUri.data(), mUriNameAddr.data());
   ret = getResourceListServer()->getSubscribeClient().
      addSubscription(mUri.data(),
                      REG_EVENT_TYPE,
                      REG_EVENT_CONTENT_TYPE,
                      getResourceListServer()->getClientFromURI(),
                      mUriNameAddr.data(),
                      getResourceListServer()->getClientContactURI(),
                      getResourceListServer()->getResubscribeInterval(),
                      getResourceListSet(),
                      ResourceListSet::subscriptionEventCallbackAsync,
                      ResourceListSet::notifyEventCallbackAsync,
                      mSubscriptionEarlyDialogHandle);
   if (ret)
   {
      OsSysLog::add(FAC_RLS, PRI_DEBUG,
                    "ContactSet:: addSubscription succeeded mUri = '%s', mSubscriptionEarlyDialogHandle = '%s'",
                    mUri.data(),
                    mSubscriptionEarlyDialogHandle.data());
      // Add this ContactSet to mSubscribeMap.
      getResourceListSet()->addSubscribeMapping(&mSubscriptionEarlyDialogHandle,
                                                this);
   }
   else
   {
      OsSysLog::add(FAC_RLS, PRI_WARNING,
                    "ContactSet:: addSubscription failed mUri = '%s', mSubscriptionEarlyDialogHandle = '%s'",
                    mUri.data(),
                    mSubscriptionEarlyDialogHandle.data());
   }
}
예제 #2
0
// Destructor
ResourceInstance::~ResourceInstance()
{
   OsSysLog::add(FAC_RLS, PRI_DEBUG,
                 "ResourceInstance::~ mInstanceName = '%s'",
                 mInstanceName.data());

   // Delete this ResourceInstance from mNotifyMap.
   getResourceListSet()->deleteNotifyMapping(&mInstanceName);

   // Terminate the subscription for this resource instance.
   UtlBoolean ret;
   ret = getResourceListServer()->getSubscribeClient().
      endSubscriptionGroup(mInstanceName.data());
   OsSysLog::add(FAC_RLS,
                 ret ? PRI_DEBUG : PRI_WARNING,
                 "ResourceInstance::~ endSubscriptionGroup %s mInstanceName = '%s'",
                 ret ? "succeeded" : "failed",
                 mInstanceName.data());

   // Delete the XML in mXmlDialogs.
   destroyXmlDialogs();

   mContentPresent = FALSE;

   // This destructor does not mark any resource lists for publication,
   // as the termination of this instance has been published previously,
   // and there is no need to publish this instance's vanishing quickly.
}
예제 #3
0
// Delete a resource identified by position.
bool ResourceList::deleteResourceAt(size_t at)
{
   bool resource_deleted = false;

   Os::Logger::instance().log(FAC_RLS, PRI_DEBUG,
                 "ResourceList::deleteResourceAt mUserPart = '%s', at = %d",
                 mUserPart.data(), (int) at);

   // Remove the ResourceReference from mResourcesList.
   ResourceReference* resource =
      dynamic_cast <ResourceReference*> (mResourcesList.removeAt(at));
   if (resource)
   {
      // Remove the URI from mChangesList.
      UtlString uri(*(resource->getUri()));
      mChangesList.destroy(&uri);

      // Delete the ResourceReference.
      resource_deleted = getResourceListSet()->getResourceCache().
                            destroyResourceReference(resource);

      // Publish the change.
      setToBePublished();
   }

   return resource_deleted;
}
예제 #4
0
// Incrementally remove and delete one component of the ResourceList.
void ResourceList::shrink(bool& listEmpty,
                          bool& resourceDeleted)
{
   resourceDeleted = false;

   Os::Logger::instance().log(FAC_RLS, PRI_DEBUG,
                 "ResourceList::shrink mUserPart = '%s'",
                 mUserPart.data());

   // Incrementally remove element from the ResourceReferences and delete it.
   // Get pointer to the first ResourceReference.
   ResourceReference* rr =
      dynamic_cast <ResourceReference*> (mResourcesList.first());

   // If one exists, delete it.
   if (rr) {
      mResourcesList.removeReference(rr);
      // A ResourceReference can be deleted outright because it contains
      // no lists.
      // It may cause deletion of the ResourceCached that it points to.
      resourceDeleted = getResourceListSet()->getResourceCache().
                           destroyResourceReference(rr);

      // Publish the change.
      setToBePublished();
   }

   // mChangesList is not handled here, as it is just a list of UtlStrings.

   listEmpty = rr == NULL;
}
예제 #5
0
// Constructor
ResourceReference::ResourceReference(ResourceList* resourceList,
                                     const char* uri,
                                     const char* nameXml,
                                     const char* display_name) :
   mResourceList(resourceList),
   mNameXml(nameXml),
   mDisplayName(display_name)
{
   // If the name XML is not empty and does not end with LF, add CR-LF.
   if (!mNameXml.isNull() && mNameXml(mNameXml.length() - 1) != '\n')
   {
      mNameXml += "\r\n";
   }
   OsSysLog::add(FAC_RLS, PRI_DEBUG,
                 "ResourceReference:: this = %p, resourceList = %p, mUri = '%s', mNameXml = '%s', mDisplayName = '%s'",
                 this, mResourceList, uri, mNameXml.data(),
                 mDisplayName.data());

   // Pass the request to the ResourceCache and save the (ResourceCached*)
   // that it returns.
   mResourceCached = getResourceListSet()->getResourceCache().
      addReferenceToResource(this, uri);

   // Publish the change to our containing ResourceList.
   resourceList->setToBePublished();
}
예제 #6
0
// Destructor
ContactSet::~ContactSet()
{
   // Delete this ContactSet from mSubscribeMap (for the "reg" subscription).
   getResourceListSet()->deleteSubscribeMapping(&mSubscriptionEarlyDialogHandle);

   // End the "reg" subscription.
   UtlBoolean ret;
   ret = getResourceListServer()->getSubscribeClient().
      endSubscriptionGroup(mSubscriptionEarlyDialogHandle);
   OsSysLog::add(FAC_RLS,
                 ret ? PRI_DEBUG : PRI_WARNING,
                 "ContactSet::~ endSubscriptionGroup %s mUri = '%s', mSubscriptionEarlyDialogHandle = '%s'",
                 ret ? "succeeded" : "failed",
                 mUri.data(),
                 mSubscriptionEarlyDialogHandle.data());

   // Remove this ContactSet from mNotifyMap for all subscriptions.
   {
      UtlHashMapIterator itor(mSubscriptions);
      UtlString* handle;
      while ((handle = dynamic_cast <UtlString*> (itor())))
      {
         getResourceListSet()->deleteNotifyMapping(handle);
      }
   }

   // Delete the contents of mSubscriptions.
   mSubscriptions.destroyAll();

   // Delete the subordinate SubscriptionSet's for the contacts.
   {
      // Have to use a loop to remove items individually, because
      // destroyAll() deadlocks with the access to mSubscriptionSets
      // during the the attempt to publish new status for the resource
      // as the ResourceInstances are recursively deleted.
      UtlHashMapIterator itor(mSubscriptionSets);
      UtlContainable* k;
      while ((k = itor()))
      {
         UtlContainable* v = itor.value();
         mSubscriptionSets.removeReference(k);
         delete k;
         delete v;
      }
   }
}
예제 #7
0
// Constructor
ResourceCached::ResourceCached(ResourceCache* resourceCache,
                               const char* uri) :
   UtlString(uri),
   mResourceCache(resourceCache),
   mContactSetP(0),
   mSeqNo(getResourceListSet()->getNextSeqNo()),
   mRefreshTimer(getResourceListServer()->getResourceListTask().
                    getMessageQueue(),
                 (void*)(mSeqNo + ResourceListSet::REFRESH_TIMEOUT))
{
   OsSysLog::add(FAC_RLS, PRI_DEBUG,
                 "ResourceCached:: this = %p, URI = '%s'",
                 this, data());

   // Add the sequence number mapping.
   getResourceListSet()->addResourceSeqNoMapping(mSeqNo, this);

   // Start subscriptions.
   startSubscriptions();
}
예제 #8
0
// Constructor
SubscriptionSet::SubscriptionSet(ResourceCached* resource,
                                 UtlString& uri) :
   ResourceSubscriptionReceiver(),
   mResource(resource),
   mUri(uri)
{
   OsSysLog::add(FAC_RLS, PRI_DEBUG,
                 "SubscriptionSet:: this = %p, resource = %p, mUri = '%s'",
                 this, mResource, mUri.data());

   // Start the subscription for dialog events.
   UtlBoolean ret;
   UtlString mUriNameAddr = "<" + mUri + ">";
   ret = getResourceListServer()->getSubscribeClient().
      addSubscription(mUri.data(),
                      getResourceListServer()->getEventType(),
                      getResourceListServer()->getContentType(),
                      getResourceListServer()->getClientFromURI(),
                      mUriNameAddr.data(),
                      getResourceListServer()->getClientContactURI(),
                      getResourceListServer()->getResubscribeInterval(),
                      getResourceListSet(),
                      ResourceListSet::subscriptionEventCallbackAsync,
                      ResourceListSet::notifyEventCallbackAsync,
                      mSubscriptionEarlyDialogHandle);
   if (ret)
   {
      OsSysLog::add(FAC_RLS, PRI_DEBUG,
                    "SubscriptionSet:: addSubscription for '%s' succeeded",
                    mUri.data());
      // Add this SubscriptionSet to mSubscribeMap.
      getResourceListSet()->addSubscribeMapping(&mSubscriptionEarlyDialogHandle,
                                                this);
   }
   else
   {
      OsSysLog::add(FAC_RLS, PRI_WARNING,
                    "SubscriptionSet:: addSubscription for '%s' failed",
                    mUri.data());
   }
}
예제 #9
0
// Destructor
ResourceCached::~ResourceCached()
{
   OsSysLog::add(FAC_RLS, PRI_DEBUG,
                 "ResourceCached::~ this = %p, URI = '%s'",
                 this, data());

   // Terminate any existing subscriptions.
   terminateSubscriptions();

   // Delete the sequence number mapping.
   getResourceListSet()->deleteResourceSeqNoMapping(mSeqNo);
}
예제 #10
0
// Declare that the contents have changed and need to be published.
void ResourceCached::setToBePublished(UtlBoolean publishNow,
                                      const UtlString* chgUri)
{
   OsSysLog::add(FAC_RLS, PRI_DEBUG,
                 "ResourceCached::setToBePublished URI = '%s'",
                 data());

   // Iterate through all our ResourceReference's and mark their
   // containing ResourceList's as needing to be published.
   UtlHashBagIterator itor(mReferences);
   ResourceReference* reference;
   while ((reference = dynamic_cast <ResourceReference*> (itor())))
   {
      reference->getResourceList()->setToBePublished(chgUri);
   }

   // If publishing is suppressed, do nothing further.
   if (!getResourceListSet()->publishingSuspended())
   {
      if (publishNow)
      {
         // If publishNow is specified, loop through the
         // ResourceList's again and publish them.  (Doing this in a
         // second loop avoids publishing a list twice if it contains
         // this resource twice.)
         UtlHashBagIterator itor(mReferences);
         ResourceReference* reference;
         while ((reference = dynamic_cast <ResourceReference*> (itor())))
         {
            reference->getResourceList()->publishIfNecessary();
         }
      }
      else
      {
         // In the more usual case, we just set the publishing timer.
         getResourceListSet()->schedulePublishing();
      }
   }
}
예제 #11
0
// Destructor
ResourceReference::~ResourceReference()
{
   OsSysLog::add(FAC_RLS, PRI_DEBUG,
                 "ResourceReference::~ this = %p, URI = '%s'",
                 this, mResourceCached->getUri()->data());

   // Tell the ResourceCache that we no longer have a reference to the
   // ResourceCached.
   getResourceListSet()->getResourceCache().
      deleteReferenceToResource(this, mResourceCached);

   // Publish the change to our containing ResourceList.
   getResourceList()->setToBePublished();
}
예제 #12
0
// Publish the contents if necessary and clear the publishing indicator.
void ResourceList::publishIfNecessary()
{
    Os::Logger::instance().log(FAC_RLS, PRI_DEBUG,
                  "ResourceList::publishIfNecessary mUserPart = '%s'",
                  mUserPart.data());

   // Do nothing if publishing is suspended.
   if (!getResourceListSet()->publishingSuspended())
   {
      if (mChangesToPublish)
      {
         mChangesToPublish = FALSE;
         publish();
      }
   }
}
예제 #13
0
// Destructor
SubscriptionSet::~SubscriptionSet()
{
   OsSysLog::add(FAC_RLS, PRI_DEBUG,
                 "SubscriptionSet::~ mUri = '%s'",
                 mUri.data());

   // Delete this SubscriptionSet from mSubscribeMap.
   getResourceListSet()->deleteSubscribeMapping(&mSubscriptionEarlyDialogHandle);

   // Terminate the master subscription.
   UtlBoolean ret;
   ret = getResourceListServer()->getSubscribeClient().
      endSubscription(mSubscriptionEarlyDialogHandle.data());
   OsSysLog::add(FAC_RLS,
                 ret ? PRI_DEBUG : PRI_WARNING,
                 "SubscriptionSet::~ endSubscription %s mUri = '%s', mSubscriptionEarlyDialogHandle = '%s'",
                 ret ? "succeeded" : "failed",
                 mUri.data(),
                 mSubscriptionEarlyDialogHandle.data());

   // Delete all the child subscriptions.

   // First, send their termination content.
   UtlSListIterator itor(mSubscriptions);
   ResourceInstance* inst;
   while ((inst = dynamic_cast <ResourceInstance*> (itor())))
   {
      // Set the state of the resource instance to terminated.
      // We do not know what their termination reason is.
      // (:TODO: But it might be possible to determine that.)
      inst->terminateContent("terminated");
   }

   // Since we are about to delete the ResourceInstances, we have to
   // publish their terminated state now or forgo doing so.
   getResourceCached()->setToBePublished(TRUE);

   // Delete the ResourceInstance objects.
   mSubscriptions.destroyAll();

   // Record that the content has been changed again and needs to be
   // published.
   getResourceCached()->setToBePublished();
}
예제 #14
0
// Constructor
ResourceInstance::ResourceInstance(SubscriptionSet* parent,
                                   const char* instanceName,
                                   const char* subscriptionState) :
   mSubscriptionSet(parent),
   mInstanceName(instanceName),
   mSubscriptionState(subscriptionState),
   mContentPresent(FALSE)
{
   OsSysLog::add(FAC_RLS, PRI_DEBUG,
                 "ResourceInstance:: mInstanceName = '%s', mSubscriptionSet = '%s', subscriptionState = '%s'",
                 mInstanceName.data(), mSubscriptionSet->getUri()->data(),
                 subscriptionState);

   // Add this ResourceInstance to mNotifyMap.
   getResourceListSet()->addNotifyMapping(mInstanceName, this);

   // Publish the state with the new instance.
   getResourceCached()->setToBePublished();
}
예제 #15
0
// Generate and publish the content for the resource list.
void ResourceList::publish()
{
   UtlBoolean publishingSuspended =
      getResourceListSet()->publishingSuspended();
   Os::Logger::instance().log(FAC_RLS, PRI_DEBUG,
                 "ResourceList::publish mUserPart = '%s', publishingSuspended = %d",
                 mUserPart.data(), publishingSuspended);

   // Do the publishing if it's not suppressed.
   if (!publishingSuspended)
   {
      // Generate and publish the RFC 4662 ("full") notice body.
      genAndPublish(FALSE, mResourceListUri);

      // Generate and publish the consolidated notice body.
      genAndPublish(TRUE, mResourceListUriCons);

      // Flush the list of partial updates.
      mChangesList.destroyAll();
   }
}
예제 #16
0
// Create and add a resource to the resource list.
void ResourceList::addResource(const char* uri,
                               const char* nameXml,
                               const char* display_name,
                               bool& resource_added,
                               bool& resource_cached_created,
                               ssize_t no_check_start,
                               ssize_t no_check_end)
{
   Os::Logger::instance().log(FAC_RLS, PRI_DEBUG,
                 "ResourceList::addResource mUserPart = '%s', uri = '%s', nameXml = '%s', display_name = '%s', no_check_start = %d, no_check_end = %d",
                 mUserPart.data(), uri, nameXml, display_name,
                 (int) no_check_start, (int) no_check_end);

   // See if 'uri' is already in the list of ResourceReference's.
   ssize_t location;
   {
      UtlSListIterator itor(mResourcesList);
      ResourceReference* r;
      // Exit this loop if 'itor' runs out of elements, or if the resource
      // URI of an element (that is not excluded) string-equals 'uri'.
      for (location = 0;
           (r = dynamic_cast <ResourceReference*> (itor())) != NULL;
           location++)
      {
         if (// If this element is not excluded from comparison and ...
             !(no_check_start <= location && location <= location) &&
             // ... it has the same URI value as the new element ...
             r->getUri()->compareTo(uri) == 0)
         {
            // ... exit the loop and return an error.
            break;
         }
      }
      // At this point, r != NULL if some ResourceReference in mResourcesList
      // that is not between no_check_start and no_check_end has
      // getUri() string-equal to 'uri'.
      resource_added = r == NULL;
   }

   resource_cached_created = false;
   if (resource_added)
   {
      ResourceReference* rr;
      getResourceListSet()->getResourceCache().
         createResourceReference(this,
                                 uri, nameXml, display_name,
                                 rr, resource_cached_created);
      mResourcesList.append(rr);

      // Publish the change.
      setToBePublished();
   }
   else
   {
      Os::Logger::instance().log(FAC_RLS, PRI_WARNING,
                    "ResourceList::addResource Resource URI '%s' is already in resource list '%s' at location %d",
                    uri, mUserPart.data(), (int) location);
   }

   Os::Logger::instance().log(FAC_RLS, PRI_DEBUG,
                 "ResourceList::addResource "
                 "resource_added = %d, resource_cached_created = %d",
                 resource_added,
                 resource_cached_created);
}
예제 #17
0
// Add to the HttpBody the current state of the resource.
void ResourceCached::generateBody(UtlString& rlmi,
                                  HttpBody& body,
                                  UtlBoolean consolidated,
                                  const UtlString& nameXml,
                                  const UtlString& displayName) const
{
   // Generate the preamble for the resource.
   rlmi += "  <resource uri=\"";
   XmlEscape(rlmi, *(static_cast <const UtlString*> (this)));
   rlmi += "\">\r\n";
   if (!nameXml.isNull())
   {
      rlmi += "    ";
      rlmi += nameXml;
   }

   if (consolidated)
   {
      // If consolidating resource instances, generate the XML for the
      // unified resource instance.
      rlmi += "    <instance id=\"consolidated\" state=\"active\"";

      UtlString contentBodyPartCid;
      // Use the count of parts in 'body' to generate a unique identifier for
      // each part.
      contentBodyPartCid.appendNumber(body.getMultipartCount());
      contentBodyPartCid += "@";
      contentBodyPartCid += getResourceListServer()->getDomainName();

      rlmi += " cid=\"";
      rlmi += contentBodyPartCid;
      rlmi += "\"";

      // Now add the <...> and use it in the header.
      contentBodyPartCid.prepend("<");
      contentBodyPartCid.append(">");

      // Create a single HttpBody to contain the unified dialog event.
      UtlString dialog_event;

      // XML declaration is optional, but Broadworks uses it.
      dialog_event += "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
      dialog_event += BEGIN_DIALOG_INFO;
      dialog_event += VERSION_EQUAL;
      dialog_event += "\"";
      // The consolidated dialog events need to have persistent
      // version numbers, as they all have the same instance id.
      // So we use a global version number in the ResourceListSet.
      dialog_event.appendNumber(getResourceListSet()->getVersion());
      dialog_event += "\"";
      dialog_event += STATE_EQUAL;
      dialog_event += "\"full\"";
      dialog_event += ENTITY_EQUAL;
      dialog_event += "\"";
      dialog_event += *(static_cast <const UtlString*> (this));
      dialog_event += "\">\r\n";
      // Save the length of dialog_event, so we can tell later if
      // any <dialog>s have been added to it.
      unsigned int preamble_length = dialog_event.length();

      // Call the ContactSet to generate the consolidated dialog event body.
      if (mContactSetP)
      {
         mContactSetP->generateBody(dialog_event, body, consolidated,
                                    displayName);
      }

      // If no <dialog>s have been added, we have to add a dummy
      // <dialog> to carry the display name.
      if (dialog_event.length() == preamble_length)
      {
         dialog_event +=
            "<dialog id=\";\"><state>terminated</state><local><identity display=\"";
         XmlEscape(dialog_event, displayName);
         dialog_event += "\">";
         XmlEscape(dialog_event, *(static_cast <const UtlString*> (this)));
         dialog_event += "</identity></local></dialog>\r\n";
      }

      dialog_event += END_DIALOG_INFO;

      // Insert the consolidated dialog event body into the multiplart body.
      HttpBody content_body(dialog_event.data(),
                            dialog_event.length(),
                            DIALOG_EVENT_CONTENT_TYPE);
      UtlDList content_body_parameters;
      content_body_parameters.append(
         new NameValuePair(HTTP_CONTENT_ID_FIELD,
                           contentBodyPartCid));
      body.appendBodyPart(content_body, content_body_parameters);
      content_body_parameters.destroyAll();

      // Finish the <instance> element.
      rlmi += "/>\r\n";
   }
   else
   {
      // Call the ContactSet to do the work.
      if (mContactSetP)
      {
         mContactSetP->generateBody(rlmi, body, consolidated, displayName);
      }
   }   
   
// Generate the postamble for the resource.
   rlmi += "  </resource>\r\n";
}
예제 #18
0
void ContactSet::subscriptionEventCallback(
   const UtlString* earlyDialogHandle,
   const UtlString* dialogHandle,
   SipSubscribeClient::SubscriptionState newState,
   const UtlString* subscriptionState)
{
   OsSysLog::add(FAC_RLS, PRI_DEBUG,
                 "ContactSet::subscriptionEventCallback mUri = '%s', newState = %d, earlyDialogHandle = '%s', dialogHandle = '%s', subscriptionState = '%s'",
                 mUri.data(),
                 newState, mSubscriptionEarlyDialogHandle.data(),
                 dialogHandle->data(), subscriptionState->data());

   switch (newState)
   {
   case SipSubscribeClient::SUBSCRIPTION_INITIATED:
      break;
   case SipSubscribeClient::SUBSCRIPTION_SETUP:
   {
         // Remember it in mSubscriptions, if there isn't already an entry.
         if (!mSubscriptions.find(dialogHandle))
         {
            // Check that we don't have too many subscriptions.
            if (mSubscriptions.entries() <
                getResourceListServer()->getMaxRegSubscInResource())
            {
               // Add this ContactSet to mNotifyMap for the subscription.
               // (::addNotifyMapping() copies *dialogHandle.)
               getResourceListSet()->addNotifyMapping(*dialogHandle, this);

               // Remember to make a copy of *dialogHandle.
               mSubscriptions.insertKeyAndValue(new UtlString(*dialogHandle),
                                                new UtlHashMap);
            }
            else
            {
               OsSysLog::add(FAC_RLS, PRI_ERR,
                             "ContactSet::subscriptionEventCallback cannot add reg subscription with dialog handle '%s', already %zu in ContactSet '%s'",
                             dialogHandle->data(), mSubscriptions.entries(),
                             mUri.data());
            }
         }
         else
         {
            OsSysLog::add(FAC_RLS, PRI_DEBUG,
                          "ContactSet::subscriptionEventCallback mSubscriptions element already exists for this dialog handle mUri = '%s', dialogHandle = '%s'",
                          mUri.data(),
                          dialogHandle->data());
         }
   }
   break;
   case SipSubscribeClient::SUBSCRIPTION_TERMINATED:
   {
      // Remove this ContactSet from mNotifyMap for the subscription.
      getResourceListSet()->deleteNotifyMapping(dialogHandle);

      // Delete this subscription from mSubscriptions.
      mSubscriptions.destroy(dialogHandle);
      // Update the subscriptions.
      updateSubscriptions();
   }
   break;
   }
}