|
|
//+---------------------------------------------------------------------------
//
// Microsoft Windows
// Copyright (C) Microsoft Corporation, 1997.
//
// File: N O T I F Y . C P P
//
// Contents: Implementation of INetConnectionNotifySink
//
// Notes:
//
// Author: shaunco 21 Aug 1998
//
//----------------------------------------------------------------------------
#include "pch.h"
#pragma hdrstop
#include "foldinc.h"
#include "nccom.h"
#include "notify.h"
#include "shutil.h"
#include "smcent.h"
#include "ctrayui.h"
#include "traymsgs.h"
#include "wzcdlg.h"
extern HWND g_hwndTray;
enum EVENT_LEVELS { EVT_LVL_DISABLE_ALL = 0, EVT_LVL_ENABLE_PRIVATE = 1, EVT_LVL_ENABLE_ALL = 2 };
DWORD g_dwCurrentEventLevel = EVT_LVL_ENABLE_ALL;
//static
HRESULT CConnectionNotifySink::CreateInstance ( IN REFIID riid, OUT VOID** ppv) { TraceFileFunc(ttidNotifySink);
HRESULT hr = E_OUTOFMEMORY;
// Initialize the output parameter.
//
*ppv = NULL;
CConnectionNotifySink* pObj; pObj = new CComObject <CConnectionNotifySink>; if (pObj) { // Do the standard CComCreator::CreateInstance stuff.
//
pObj->SetVoid (NULL); pObj->InternalFinalConstructAddRef (); hr = pObj->FinalConstruct (); pObj->InternalFinalConstructRelease ();
if (SUCCEEDED(hr)) { // Call the PidlInitialize function to allow the enumeration
// object to copy the list.
//
hr = HrGetConnectionsFolderPidl(pObj->m_pidlFolder);
if (SUCCEEDED(hr)) { hr = pObj->QueryInterface (riid, ppv); } }
if (FAILED(hr)) { delete pObj; } }
TraceHr(ttidError, FAL, hr, (S_FALSE == hr), "CConnectionNotifySink::CreateInstance"); return hr; }
//+---------------------------------------------------------------------------
//
// Member: CConnectionNotifySink::~CConnectionNotifySink
//
// Purpose: Clean up the sink object, deleting the folder pidl and any
// alloc'd junk we might add in the future.
//
// Arguments:
// (none)
//
// Returns:
//
// Author: jeffspr 26 Aug 1998
//
// Notes:
//
CConnectionNotifySink::~CConnectionNotifySink() throw() { TraceFileFunc(ttidNotifySink);
if (FIsDebugFlagSet (dfidBreakOnNotifySinkRelease)) { AssertSz(FALSE, "THIS IS NOT A BUG! The debug flag " "\"BreakOnNotifySinkRelease\" has been set. Set your breakpoints now."); } TraceTag(ttidNotifySink, "Connection notify sink destroyed"); }
HRESULT CConnectionNotifySink::HrUpdateIncomingConnection() { HRESULT hr = S_OK;
ConnListEntry cle; hr = g_ccl.HrFindRasServerConnection(cle); if (S_OK == hr) { hr = HrOnNotifyUpdateConnection( m_pidlFolder, &(cle.ccfe.GetGuidID()), cle.ccfe.GetNetConMediaType(), cle.ccfe.GetNetConSubMediaType(), cle.ccfe.GetNetConStatus(), cle.ccfe.GetCharacteristics(), cle.ccfe.GetName(), cle.ccfe.GetDeviceName(), cle.ccfe.GetPhoneOrHostAddress() );
if (SUCCEEDED(hr)) { PCONFOLDPIDL pidlShellNotify; hr = cle.ccfe.ConvertToPidl(pidlShellNotify); if (SUCCEEDED(hr)) { GenerateEvent(SHCNE_UPDATEITEM, m_pidlFolder, pidlShellNotify, NULL); } } }
return hr; }
HRESULT CConnectionNotifySink::ConnectionAdded ( IN const NETCON_PROPERTIES_EX* pPropsEx) { TraceFileFunc(ttidNotifySink);
#ifdef DBG
OLECHAR szGuidString[MAX_GUID_STRING_LEN]; StringFromGUID2(pPropsEx->guidId, szGuidString, MAX_GUID_STRING_LEN); TraceTag(ttidNotifySink, "INetConnectionNotifySink::ConnectionAdded - %S %S [%s:%s:%s:%s]", szGuidString, pPropsEx->bstrName, DbgNcm(pPropsEx->ncMediaType), DbgNcsm(pPropsEx->ncSubMediaType), DbgNcs(pPropsEx->ncStatus), DbgNccf(pPropsEx->dwCharacter) ); #endif
if (g_dwCurrentEventLevel <= EVT_LVL_DISABLE_ALL) { TraceTag(ttidNotifySink, "Last event ignored due to g_dwCurrentEventLevel == %d", g_dwCurrentEventLevel); return S_FALSE; }
HRESULT hr = S_OK; // Only used for code paths. We don't return this.
HRESULT hrFind = S_OK; // Only for finding the connection.
PCONFOLDPIDL pidlNew;
ConnListEntry cleDontCare; hrFind = g_ccl.HrFindConnectionByGuid(&(pPropsEx->guidId), cleDontCare); if (S_OK == hrFind) { // We already know about this connection. Don't bother added it.
TraceTag(ttidShellFolder, "Ignoring known connection on ConnectionAdded notify"); } else { // Create the entry in the connection list and get the returned pidl
//
hr = HrCreateConFoldPidl( *pPropsEx, pidlNew); if (SUCCEEDED(hr) && (!pidlNew.empty())) { CONFOLDENTRY cfe;
Assert(!m_pidlFolder.empty());
// Convert to the confoldentry
//
hr = pidlNew.ConvertToConFoldEntry(cfe); if (SUCCEEDED(hr)) { // Insert the connection in the connection list
//
hr = g_ccl.HrInsert(cfe); if (SUCCEEDED(hr)) { // Notify the shell that we have a new object
//
PCONFOLDPIDL pidlShellNotify; hr = cfe.ConvertToPidl(pidlShellNotify); if (SUCCEEDED(hr)) { GenerateEvent(SHCNE_CREATE, m_pidlFolder, pidlShellNotify, NULL); } }
// Don't delete the cfe here because the connection list now owns it.
} } }
if (SUCCEEDED(hr) && (NCM_NONE != pPropsEx->ncMediaType) && (pPropsEx->dwCharacter & NCCF_INCOMING_ONLY) ) { hr = HrUpdateIncomingConnection(); }
return S_OK; }
HRESULT CConnectionNotifySink::ConnectionBandWidthChange ( IN const GUID* pguidId) { TraceFileFunc(ttidNotifySink); #ifdef DBG
OLECHAR szGuidString[MAX_GUID_STRING_LEN]; StringFromGUID2(*pguidId, szGuidString, MAX_GUID_STRING_LEN); TraceTag(ttidNotifySink, "INetConnectionNotifySink::ConnectionBandWidthChange - %S", szGuidString); #endif
if (g_dwCurrentEventLevel <= EVT_LVL_DISABLE_ALL) { TraceTag(ttidNotifySink, "Last event ignored due to g_dwCurrentEventLevel == %d", g_dwCurrentEventLevel); return S_FALSE; }
HRESULT hr = S_OK; CNetStatisticsCentral * pnsc = NULL;
hr = CNetStatisticsCentral::HrGetNetStatisticsCentral(&pnsc, FALSE); if (SUCCEEDED(hr)) { pnsc->UpdateRasLinkList(pguidId); ReleaseObj(pnsc); }
return S_OK; }
HRESULT CConnectionNotifySink::ConnectionDeleted ( IN const GUID* pguidId) { TraceFileFunc(ttidNotifySink);
HRESULT hr = S_OK; HRESULT hrFind = S_OK; ConnListEntry cle; PCONFOLDPIDL pidlFind; BOOL fFlushPosts = FALSE;
Assert(pguidId); Assert(!m_pidlFolder.empty());
if (g_dwCurrentEventLevel <= EVT_LVL_DISABLE_ALL) { TraceTag(ttidNotifySink, "CConnectionNotifySink::ConnectionDeleted event ignored due to g_dwCurrentEventLevel == %d", g_dwCurrentEventLevel); return S_FALSE; }
// Find the connection using the GUID.
//
hrFind = g_ccl.HrFindConnectionByGuid(pguidId, cle); if (S_OK == hrFind) { #ifdef DBG
OLECHAR szGuidString[MAX_GUID_STRING_LEN]; StringFromGUID2(cle.ccfe.GetGuidID(), szGuidString, MAX_GUID_STRING_LEN); TraceTag(ttidNotifySink, "INetConnectionNotifySink::ConnectionDeleted - %S %S [%s:%s:%s:%s]", szGuidString, cle.ccfe.GetName(), DbgNcm(cle.ccfe.GetNetConMediaType()), DbgNcsm(cle.ccfe.GetNetConSubMediaType()), DbgNcs(cle.ccfe.GetNetConStatus()), DbgNccf(cle.ccfe.GetCharacteristics())); #endif
// Very important to release the lock before doing any thing which
// calls back into the shell. (e.g. GenerateEvent)
const CONFOLDENTRY &ccfe = cle.ccfe;
// Get the pidl for the connection so we can use it to notify
// the shell further below.
//
ccfe.ConvertToPidl(pidlFind);
// Remove this connection from the global list while we
// have the lock held.
//
hr = g_ccl.HrRemove(cle.ccfe, &fFlushPosts); } else { TraceTag(ttidShellFolder, "Notify: Delete <item not found in cache>. hr = 0x%08x", hr); }
// If we need to flush the posts after making tray icon changes, do so
//
if (g_hwndTray && fFlushPosts) { FlushTrayPosts(g_hwndTray); }
if (SUCCEEDED(hr) && (!pidlFind.empty())) { GenerateEvent(SHCNE_DELETE, m_pidlFolder, pidlFind, NULL); }
if (SUCCEEDED(hr)) { hr = HrUpdateIncomingConnection(); }
// Only return S_OK from here.
//
return S_OK; }
HRESULT CConnectionNotifySink::ConnectionModified ( IN const NETCON_PROPERTIES_EX* pPropsEx) { TraceFileFunc(ttidNotifySink);
Assert(pPropsEx); Assert(!m_pidlFolder.empty());
#ifdef DBG
OLECHAR szGuidString[MAX_GUID_STRING_LEN]; StringFromGUID2(pPropsEx->guidId, szGuidString, MAX_GUID_STRING_LEN); TraceTag(ttidNotifySink, "INetConnectionNotifySink::ConnectionModified - %S %S [%s:%s:%s:%s]", szGuidString, pPropsEx->bstrName, DbgNcm(pPropsEx->ncMediaType), DbgNcsm(pPropsEx->ncSubMediaType), DbgNcs(pPropsEx->ncStatus), DbgNccf(pPropsEx->dwCharacter)); #endif
if (g_dwCurrentEventLevel <= EVT_LVL_ENABLE_PRIVATE) { TraceTag(ttidNotifySink, "Last event ignored due to g_dwCurrentEventLevel == %d", g_dwCurrentEventLevel); return S_FALSE; }
// Get the result for debugging only. We never want to fail this function
//
HRESULT hrTmp = HrOnNotifyUpdateConnection( m_pidlFolder, (GUID *)&(pPropsEx->guidId), pPropsEx->ncMediaType, pPropsEx->ncSubMediaType, pPropsEx->ncStatus, pPropsEx->dwCharacter, pPropsEx->bstrName, pPropsEx->bstrDeviceName, pPropsEx->bstrPhoneOrHostAddress);
return S_OK; }
HRESULT CConnectionNotifySink::ConnectionRenamed ( IN const GUID* pguidId, IN PCWSTR pszwNewName) { TraceFileFunc(ttidNotifySink);
HRESULT hr = S_OK; PCONFOLDPIDL pidlNew;
Assert(pguidId); Assert(pszwNewName);
if (g_dwCurrentEventLevel <= EVT_LVL_DISABLE_ALL) { TraceTag(ttidNotifySink, "CConnectionNotifySink::ConnectionRenamed event ignored due to g_dwCurrentEventLevel == %d", g_dwCurrentEventLevel); return S_FALSE; }
// Update the name in the cache
//
// Note: There exists a race condition with shutil.cpp:
// HrRenameConnectionInternal\HrUpdateNameByGuid can also update the cache.
ConnListEntry cle; hr = g_ccl.HrFindConnectionByGuid(pguidId, cle); if (FAILED(hr)) { return E_INVALIDARG; } #ifdef DBG
OLECHAR szGuidString[MAX_GUID_STRING_LEN]; StringFromGUID2(cle.ccfe.GetGuidID(), szGuidString, MAX_GUID_STRING_LEN); TraceTag(ttidNotifySink, "INetConnectionNotifySink::ConnectionRenamed - %S %S (to %S) [%s:%s:%s:%s]", szGuidString, cle.ccfe.GetName(), pszwNewName, DbgNcm(cle.ccfe.GetNetConMediaType()), DbgNcsm(cle.ccfe.GetNetConSubMediaType()), DbgNcs(cle.ccfe.GetNetConStatus()), DbgNccf(cle.ccfe.GetCharacteristics())); #endif
hr = g_ccl.HrUpdateNameByGuid( (GUID *) pguidId, (PWSTR) pszwNewName, pidlNew, TRUE); // Force the update -- this is a notification, not a request
if (S_OK == hr) { PCONFOLDPIDL pidl; cle.ccfe.ConvertToPidl(pidl);
GenerateEvent( SHCNE_RENAMEITEM, m_pidlFolder, pidl, pidlNew.GetItemIdList());
// Update status monitor title (RAS case)
CNetStatisticsCentral * pnsc = NULL;
hr = CNetStatisticsCentral::HrGetNetStatisticsCentral(&pnsc, FALSE); if (SUCCEEDED(hr)) { pnsc->UpdateTitle(pguidId, pszwNewName); ReleaseObj(pnsc); } } else { // If the connection wasn't found in the cache, then it's likely that
// the notification engine is giving us a notification for a connection
// that hasn't yet been given to us.
//
if (S_FALSE == hr) { TraceHr(ttidShellFolder, FAL, hr, FALSE, "Rename notification received on a connection we don't know about"); } }
return S_OK; }
HRESULT CConnectionNotifySink::ConnectionStatusChange ( IN const GUID* pguidId, IN NETCON_STATUS Status) { TraceFileFunc(ttidNotifySink);
HRESULT hr = S_OK; HRESULT hrFind = S_OK; PCONFOLDPIDL pidlFind;
if (g_dwCurrentEventLevel <= EVT_LVL_ENABLE_PRIVATE) { TraceTag(ttidNotifySink, "CConnectionNotifySink::ConnectionStatusChange event ignored due to g_dwCurrentEventLevel == %d", g_dwCurrentEventLevel); return S_FALSE; } // Find the connection using the GUID. Cast the const away from the GUID
//
hrFind = g_ccl.HrFindPidlByGuid((GUID *) pguidId, pidlFind);
if( S_OK != hrFind ) { GUID guidOwner;
// We did not find the guid in connection folder. Try finding the connection in the
// hidden connectiod list on netmans side.
//
hr = g_ccl.HrMapCMHiddenConnectionToOwner(*pguidId, &guidOwner); if (S_OK == hr) { // The conection has a parent!!!! Use the childs status instead of the parents status.
//
if (Status == NCS_CONNECTED) { // This means that the child has connected and the parent still has to connect
// the overall status should stay as connected. This was done to overrule the Multi-link
// hack in HrOnNotifyUpdateStatus. If we did not do this it would say :
// Child( Connecting, Connected) and then Parent(Connecting, Connected)
//
Status = NCS_CONNECTING; }
// Get the pidl of the parent.
//
hrFind = g_ccl.HrFindPidlByGuid(&guidOwner, pidlFind); } }
if (S_OK == hrFind) { #ifdef DBG
OLECHAR szGuidString[MAX_GUID_STRING_LEN]; StringFromGUID2(pidlFind->guidId, szGuidString, MAX_GUID_STRING_LEN); TraceTag(ttidNotifySink, "INetConnectionNotifySink::ConnectionStatusChange - %S %S [%s:%s:%s:%s]", szGuidString, pidlFind->PszGetNamePointer(), DbgNcm(pidlFind->ncm), DbgNcsm(pidlFind->ncsm), DbgNcs(Status), DbgNccf(pidlFind->dwCharacteristics)); #endif
hr = HrOnNotifyUpdateStatus(m_pidlFolder, pidlFind, Status); }
return S_OK; }
HRESULT CConnectionNotifySink::RefreshAll () { TraceFileFunc(ttidNotifySink);
#ifdef DBG
TraceTag(ttidNotifySink, "INetConnectionNotifySink::RefreshAll"); #endif
if (g_dwCurrentEventLevel <= EVT_LVL_DISABLE_ALL) { TraceTag(ttidNotifySink, "Last event ignored due to g_dwCurrentEventLevel == %d", g_dwCurrentEventLevel); return S_FALSE; }
// Refresh the connections folder, without having to hook the shell view.
// In this case, we do a non-flush refresh where we compare the new set
// of items to the cached set and do the merge (with the correct set
// of individual notifications).
//
(VOID) HrForceRefreshNoFlush(m_pidlFolder);
return S_OK; }
HRESULT CConnectionNotifySink::ConnectionAddressChange ( IN const GUID* pguidId ) { // Find the connection using the GUID.
//
PCONFOLDPIDL pidlFind; HRESULT hr = g_ccl.HrFindPidlByGuid(pguidId, pidlFind); if (S_OK != hr) { return E_INVALIDARG; }
#ifdef DBG
OLECHAR szGuidString[MAX_GUID_STRING_LEN]; StringFromGUID2(pidlFind->guidId, szGuidString, MAX_GUID_STRING_LEN); TraceTag(ttidNotifySink, "INetConnectionNotifySink::ConnectionAddressChange - %S %S [%s:%s:%s:%s]", szGuidString, pidlFind->PszGetNamePointer(), DbgNcm(pidlFind->ncm), DbgNcsm(pidlFind->ncsm), DbgNcs(pidlFind->ncs), DbgNccf(pidlFind->dwCharacteristics));
#endif
if (g_dwCurrentEventLevel <= EVT_LVL_DISABLE_ALL) { TraceTag(ttidNotifySink, "Last event ignored due to g_dwCurrentEventLevel == %d", g_dwCurrentEventLevel); return S_FALSE; }
PCONFOLDPIDLFOLDER pidlFolder; hr = HrGetConnectionsFolderPidl(pidlFolder); if (SUCCEEDED(hr)) { GenerateEvent(SHCNE_UPDATEITEM, pidlFolder, pidlFind, NULL); } return hr; }
DWORD WINAPI OnTaskBarIconBalloonClickThread(IN LPVOID lpParam);
STDMETHODIMP CConnectionNotifySink::ShowBalloon( IN const GUID* pguidId, IN const BSTR szCookie, IN const BSTR szBalloonText) { HRESULT hr;
#ifdef DBG
OLECHAR szGuidString[MAX_GUID_STRING_LEN]; StringFromGUID2(*pguidId, szGuidString, MAX_GUID_STRING_LEN); TraceTag(ttidNotifySink, "INetConnectionNotifySink::ShowBalloon - %S (%S)", szGuidString, szBalloonText); #endif
if (g_dwCurrentEventLevel <= EVT_LVL_DISABLE_ALL) { TraceTag(ttidNotifySink, "Last event ignored due to g_dwCurrentEventLevel == %d", g_dwCurrentEventLevel); return S_FALSE; }
CComBSTR szBalloonTextTmp = szBalloonText; BSTR szCookieTmp = NULL;
if (szCookie) { szCookieTmp = SysAllocStringByteLen(reinterpret_cast<LPCSTR>(szCookie), SysStringByteLen(szCookie));
if (szCookieTmp) { ConnListEntry cleFind; hr = g_ccl.HrFindConnectionByGuid(pguidId, cleFind); if (S_OK == hr) { hr = WZCCanShowBalloon(pguidId, cleFind.ccfe.GetName(), &szBalloonTextTmp, &szCookieTmp); if ( (S_OK == hr) || (S_OBJECT_NO_LONGER_VALID == hr) ) { CTrayBalloon *pTrayBalloon = new CTrayBalloon(); if (!pTrayBalloon) { hr = E_OUTOFMEMORY; } else { pTrayBalloon->m_gdGuid = *pguidId; pTrayBalloon->m_dwTimeOut = 30 * 1000; pTrayBalloon->m_szMessage = szBalloonTextTmp; pTrayBalloon->m_pfnFuncCallback = WZCOnBalloonClick; pTrayBalloon->m_szCookie = szCookieTmp; pTrayBalloon->m_szAdapterName = cleFind.ccfe.GetName();
if (S_OK == hr) { PostMessage(g_hwndTray, MYWM_SHOWBALLOON, NULL, (LPARAM) pTrayBalloon); } else // S_OBJECT_NO_LONGER_VALID == hr
{ CreateThread(NULL, STACK_SIZE_SMALL, OnTaskBarIconBalloonClickThread, pTrayBalloon, 0, NULL); } } } } if (S_OK != hr && S_OBJECT_NO_LONGER_VALID != hr) { SysFreeString(szCookieTmp); } } }
TraceHr(ttidError, FAL, hr, FALSE, "CConnectionNotifySink::ShowBalloon");
return hr; }
UINT_PTR uipTimer = NULL;
VOID CALLBACK EventTimerProc( IN HWND hwnd, // handle to window
IN UINT uMsg, // WM_TIMER message
IN UINT_PTR idEvent, // timer identifier
IN DWORD dwTime // current system time
) { HRESULT hr = S_OK; TraceTag(ttidNotifySink, "Refreshing the folder due to DisableEvents timeout reached");
g_dwCurrentEventLevel = EVT_LVL_ENABLE_ALL; if (uipTimer) { KillTimer(NULL, uipTimer); uipTimer = NULL; } PCONFOLDPIDLFOLDER pcfpFolder; hr = HrGetConnectionsFolderPidl(pcfpFolder); if (SUCCEEDED(hr)) { HrForceRefreshNoFlush(pcfpFolder); } }
STDMETHODIMP CConnectionNotifySink::DisableEvents ( IN const BOOL fDisable, IN const ULONG ulDisableTimeout) { #ifdef DBG
TraceTag(ttidNotifySink, "INetConnectionNotifySink::DisableEvents - %s 0x%08x", fDisable ? "DISABLE" : "ENABLE", ulDisableTimeout); #endif
HRESULT hr = S_OK;
if (fDisable) { if (HIWORD(ulDisableTimeout) & 0x8000) { // Called from private interface - disable all the events
g_dwCurrentEventLevel = EVT_LVL_DISABLE_ALL; } else { // Called from public interface - only disable connection modified & status change events
g_dwCurrentEventLevel = EVT_LVL_ENABLE_PRIVATE; }
UINT uiEventTimeOut = LOWORD(ulDisableTimeout); if (uipTimer) { KillTimer(NULL, uipTimer); uipTimer = NULL; }
uipTimer = SetTimer(NULL, NULL, uiEventTimeOut, EventTimerProc); } else { g_dwCurrentEventLevel = EVT_LVL_ENABLE_ALL;
if (uipTimer) { KillTimer(NULL, uipTimer); uipTimer = NULL; } else { hr = S_FALSE; // Timer no more.
}
HrForceRefreshNoFlush(m_pidlFolder); } return hr; }
//+---------------------------------------------------------------------------
//
// Function: HrGetNotifyConPoint
//
// Purpose: Common code for getting the connection point for use in
// NotifyAdd and NotifyRemove
//
// Arguments:
// ppConPoint [out] Return ptr for IConnectionPoint
//
// Returns:
//
// Author: jeffspr 24 Aug 1998
//
// Notes:
//
HRESULT HrGetNotifyConPoint( IConnectionPoint ** ppConPoint) { TraceFileFunc(ttidNotifySink);
HRESULT hr = S_OK; IConnectionPointContainer * pContainer = NULL;
Assert(ppConPoint);
// Get the debug interface from the connection manager
//
hr = HrCreateInstance( CLSID_ConnectionManager, CLSCTX_LOCAL_SERVER | CLSCTX_NO_CODE_DOWNLOAD, &pContainer);
TraceHr(ttidError, FAL, hr, FALSE, "HrCreateInstance(CLSID_ConnectionManager) for IConnectionPointContainer");
if (SUCCEEDED(hr)) { IConnectionPoint * pConPoint = NULL; // Get the connection point itself and fill in the return param
// on success
//
hr = pContainer->FindConnectionPoint( IID_INetConnectionNotifySink, &pConPoint);
TraceHr(ttidError, FAL, hr, FALSE, "pContainer->FindConnectionPoint");
if (SUCCEEDED(hr)) { NcSetProxyBlanket (pConPoint); *ppConPoint = pConPoint; }
ReleaseObj(pContainer); }
TraceHr(ttidError, FAL, hr, FALSE, "HrGetNotifyConPoint"); return hr; }
|