506 lines
20 KiB
C++
506 lines
20 KiB
C++
|
//***************************************************************************
|
||
|
//
|
||
|
// (c) 1999-2001 by Microsoft Corp. All Rights Reserved.
|
||
|
//
|
||
|
// sqlcache.cpp
|
||
|
//
|
||
|
// cvadai 6-May-1999 created.
|
||
|
//
|
||
|
//***************************************************************************
|
||
|
|
||
|
#define _SQLCACHE_CPP_
|
||
|
#pragma warning( disable : 4786 ) // identifier was truncated to 'number' characters in the
|
||
|
#pragma warning( disable : 4251 ) // needs to have dll-interface to be used by clients of class
|
||
|
#include "precomp.h"
|
||
|
|
||
|
#include <std.h>
|
||
|
#include <smrtptr.h>
|
||
|
#include <reputils.h>
|
||
|
#include <sqlutils.h>
|
||
|
#include <sqlcache.h>
|
||
|
#include <sqlit.h>
|
||
|
#include <repdrvr.h>
|
||
|
#include <wbemint.h>
|
||
|
|
||
|
#if defined _WIN64
|
||
|
#define ULONG unsigned __int64
|
||
|
#define LONG __int64
|
||
|
#endif
|
||
|
|
||
|
//***************************************************************************
|
||
|
//
|
||
|
// COLEDBConnection::COLEDBConnection
|
||
|
//
|
||
|
//***************************************************************************
|
||
|
|
||
|
COLEDBConnection::COLEDBConnection (IDBInitialize *pDBInit)
|
||
|
{
|
||
|
m_pDBInit = pDBInit;
|
||
|
m_tCreateTime = time(0);
|
||
|
m_bInUse = false;
|
||
|
m_pSession = NULL;
|
||
|
m_pCmd = NULL;
|
||
|
m_pTrans = NULL;
|
||
|
}
|
||
|
|
||
|
//***************************************************************************
|
||
|
//
|
||
|
// COLEDBConnection::~COLEDBConnection
|
||
|
//
|
||
|
//***************************************************************************
|
||
|
COLEDBConnection::~COLEDBConnection ()
|
||
|
{
|
||
|
if (m_pDBInit)
|
||
|
{
|
||
|
m_ObjMgr.Empty();
|
||
|
if (m_pTrans)
|
||
|
m_pTrans->Release();
|
||
|
if (m_pCmd) m_pCmd->Release();
|
||
|
if (m_pSession)
|
||
|
m_pSession->Release();
|
||
|
m_pDBInit->Uninitialize();
|
||
|
while (m_pDBInit->Release());
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
//***************************************************************************
|
||
|
//
|
||
|
// CSQLExecuteRepdrvr::GetNextResultRows
|
||
|
//
|
||
|
//***************************************************************************
|
||
|
|
||
|
HRESULT CSQLExecuteRepdrvr::GetNextResultRows(int iNumRows, IRowset *pIRowset, IMalloc *pMalloc, IWbemClassObject *pNewObj, CSchemaCache *pSchema
|
||
|
, CWmiDbSession *pSession, Properties &PropIds, bool *bImage, bool bOnImage)
|
||
|
{
|
||
|
HRESULT hr = WBEM_S_NO_ERROR;
|
||
|
ULONG nCols;
|
||
|
DBCOLUMNINFO* pColumnsInfo = NULL;
|
||
|
OLECHAR* pColumnStrings = NULL;
|
||
|
ULONG nCol;
|
||
|
ULONG cRowsObtained; // Count of rows
|
||
|
// obtained
|
||
|
HROW *rghRows = new HROW[iNumRows]; // Row handles
|
||
|
HROW* pRows = &rghRows[0]; // Pointer to the row
|
||
|
// handles
|
||
|
CDeleteMe <HROW> r2 (rghRows);
|
||
|
IAccessor* pIAccessor; // Pointer to the
|
||
|
// accessor
|
||
|
HACCESSOR hAccessor; // Accessor handle
|
||
|
DBBINDSTATUS* pDBBindStatus = NULL;
|
||
|
DBBINDING* pDBBindings = NULL;
|
||
|
BYTE* pRowValues = NULL;
|
||
|
LPWSTR lpColumnName;
|
||
|
|
||
|
// Get Column Info
|
||
|
|
||
|
IColumnsInfo* pIColumnsInfo;
|
||
|
|
||
|
pIRowset->QueryInterface(IID_IColumnsInfo, (void**) &pIColumnsInfo);
|
||
|
hr = pIColumnsInfo->GetColumnInfo(&nCols, &pColumnsInfo, &pColumnStrings);
|
||
|
CReleaseMe r1 (pIColumnsInfo);
|
||
|
|
||
|
if (nCols > 11 || nCols < 10)
|
||
|
return WBEM_E_INVALID_QUERY;
|
||
|
|
||
|
|
||
|
// Create the binding structures
|
||
|
ULONG cbRow = 0;
|
||
|
|
||
|
pDBBindings = new DBBINDING[nCols];
|
||
|
CDeleteMe <DBBINDING> d1(pDBBindings);
|
||
|
|
||
|
if (!pDBBindings || !rghRows)
|
||
|
return WBEM_E_OUT_OF_MEMORY;
|
||
|
|
||
|
for (nCol = 0; nCol < nCols; nCol++)
|
||
|
{
|
||
|
pDBBindings[nCol].iOrdinal = nCol+1;
|
||
|
pDBBindings[nCol].obValue = cbRow;
|
||
|
pDBBindings[nCol].obLength = 0;
|
||
|
pDBBindings[nCol].obStatus = 0;
|
||
|
pDBBindings[nCol].pTypeInfo = NULL;
|
||
|
pDBBindings[nCol].pObject = NULL;
|
||
|
pDBBindings[nCol].pBindExt = NULL;
|
||
|
pDBBindings[nCol].dwPart = DBPART_VALUE;
|
||
|
pDBBindings[nCol].dwMemOwner = DBMEMOWNER_CLIENTOWNED;
|
||
|
pDBBindings[nCol].eParamIO = DBPARAMIO_NOTPARAM;
|
||
|
pDBBindings[nCol].wType = pColumnsInfo[nCol].wType;
|
||
|
|
||
|
if (nCol == 10)
|
||
|
{
|
||
|
pDBBindings[nCol].wType = DBTYPE_WSTR; // 64-bit ints must end up as strings.
|
||
|
pDBBindings[nCol].cbMaxLen = 50;
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
if (pDBBindings[nCol].wType == DBTYPE_WSTR ||
|
||
|
pDBBindings[nCol].wType == DBTYPE_STR ||
|
||
|
pDBBindings[nCol].wType == DBTYPE_BSTR)
|
||
|
pDBBindings[nCol].cbMaxLen = pColumnsInfo[nCol].ulColumnSize+1;
|
||
|
else
|
||
|
if (pColumnsInfo[nCol].ulColumnSize > 65535)
|
||
|
pDBBindings[nCol].cbMaxLen = 65535;
|
||
|
else
|
||
|
pDBBindings[nCol].cbMaxLen = pColumnsInfo[nCol].ulColumnSize;
|
||
|
}
|
||
|
|
||
|
pDBBindings[nCol].dwFlags = 0;
|
||
|
pDBBindings[nCol].bPrecision = pColumnsInfo[nCol].bPrecision;
|
||
|
pDBBindings[nCol].bScale = pColumnsInfo[nCol].bScale;
|
||
|
|
||
|
lpColumnName = pColumnsInfo[nCol].pwszName;
|
||
|
|
||
|
cbRow += pDBBindings[nCol].cbMaxLen;
|
||
|
}
|
||
|
|
||
|
pRowValues = new BYTE[cbRow];
|
||
|
pDBBindStatus = new DBBINDSTATUS[nCols];
|
||
|
CDeleteMe <BYTE> r3 (pRowValues);
|
||
|
CDeleteMe <DBBINDSTATUS> r4 (pDBBindStatus);
|
||
|
|
||
|
if (!pRowValues || !pDBBindStatus)
|
||
|
return WBEM_E_OUT_OF_MEMORY;
|
||
|
|
||
|
pIRowset->QueryInterface(IID_IAccessor, (void**) &pIAccessor);
|
||
|
CReleaseMe r7 (pIAccessor);
|
||
|
pIAccessor->CreateAccessor(
|
||
|
DBACCESSOR_ROWDATA,// Accessor will be used to retrieve row
|
||
|
// data
|
||
|
nCols, // Number of columns being bound
|
||
|
pDBBindings, // Structure containing bind info
|
||
|
0,
|
||
|
&hAccessor, // Returned accessor handle
|
||
|
pDBBindStatus // Information about binding validity
|
||
|
);
|
||
|
|
||
|
// Get the next row.
|
||
|
|
||
|
hr = pIRowset->GetNextRows( 0, // Reserved
|
||
|
0, // cRowsToSkip
|
||
|
iNumRows, // cRowsDesired
|
||
|
&cRowsObtained, // cRowsObtained
|
||
|
&pRows ); // Filled in w/ row handles.
|
||
|
if (FAILED(hr))
|
||
|
{
|
||
|
// SetWMIOLEDBError(pIRowset);
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
if (cRowsObtained > 0)
|
||
|
{
|
||
|
for (int i = 0; i < cRowsObtained; i++)
|
||
|
{
|
||
|
pIRowset->GetData(rghRows[i], hAccessor, pRowValues);
|
||
|
|
||
|
DWORD dwPropertyId, dwStorage, dwPosition, dwCIMType, dwFlags, dwPropFlags;
|
||
|
_bstr_t sPropName;
|
||
|
SQL_ID dRefID = 0;
|
||
|
VARIANT vValue;
|
||
|
CClearMe c (&vValue);
|
||
|
wchar_t wTemp[455];
|
||
|
|
||
|
// We should have exactly 13 columns:
|
||
|
// (ObjectId, ClassId, PropertyId, ArrayPos, PropertyStringValue,
|
||
|
// PropertyNumericValue, PropertyRealValue, Flags, ClassId, ObjectPath,
|
||
|
// RefId, RefClassId, string binding of PropertyNumericValue
|
||
|
// ===================================================================
|
||
|
|
||
|
SQL_ID dOrigClassId = 0;
|
||
|
|
||
|
if (&pRowValues[pDBBindings[0].obValue] != NULL)
|
||
|
{
|
||
|
if (pDBBindings[1].wType == DBTYPE_NUMERIC)
|
||
|
{
|
||
|
DB_NUMERIC *pTemp = (DB_NUMERIC *)&pRowValues[pDBBindings[1].obValue];
|
||
|
dOrigClassId = CSQLExecute::GetInt64(pTemp);
|
||
|
}
|
||
|
else
|
||
|
dOrigClassId = *((long *)&pRowValues[pDBBindings[1].obValue]);;
|
||
|
}
|
||
|
|
||
|
dwPropertyId = *((long *)&pRowValues[pDBBindings[2].obValue]);
|
||
|
dwPosition = *((short *)&pRowValues[pDBBindings[3].obValue]);
|
||
|
dwFlags = *((short *)&pRowValues[pDBBindings[7].obValue]) == NULL ? 0 : *((short *)&pRowValues[pDBBindings[7].obValue]);
|
||
|
|
||
|
hr = pSchema->GetPropertyInfo (dwPropertyId, &sPropName, NULL, &dwStorage,
|
||
|
&dwCIMType, &dwPropFlags);
|
||
|
if (FAILED(hr))
|
||
|
break;
|
||
|
|
||
|
BYTE *pBuffer = NULL;
|
||
|
DWORD dwLen = 0;
|
||
|
|
||
|
switch (dwStorage)
|
||
|
{
|
||
|
case WMIDB_STORAGE_STRING:
|
||
|
if (bOnImage)
|
||
|
{
|
||
|
hr = ReadImageValue(pIRowset, 5, &pRows, &pBuffer, dwLen);
|
||
|
SetVariant(CIM_STRING, &vValue, pBuffer, dwCIMType);
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
SetVariant(CIM_STRING, &vValue, &pRowValues[pDBBindings[4].obValue], dwCIMType);
|
||
|
int iLen = wcslen(vValue.bstrVal);
|
||
|
if (bImage)
|
||
|
*bImage = (IsTruncated(vValue.bstrVal) ? true : false);
|
||
|
}
|
||
|
|
||
|
break;
|
||
|
case WMIDB_STORAGE_NUMERIC:
|
||
|
if (dwCIMType == CIM_UINT64 || dwCIMType == CIM_SINT64)
|
||
|
SetVariant(CIM_STRING, &vValue, &pRowValues[pDBBindings[10].obValue], dwCIMType);
|
||
|
else
|
||
|
{
|
||
|
if (pDBBindings[5].wType == DBTYPE_NUMERIC)
|
||
|
SetVariant(DBTYPE_NUMERIC, &vValue, &pRowValues[pDBBindings[5].obValue], dwCIMType);
|
||
|
else
|
||
|
SetVariant(VT_I4, &vValue, &pRowValues[pDBBindings[5].obValue], dwCIMType);
|
||
|
}
|
||
|
break;
|
||
|
case WMIDB_STORAGE_REAL:
|
||
|
SetVariant(VT_R8, &vValue, &pRowValues[pDBBindings[6].obValue], dwCIMType);
|
||
|
break;
|
||
|
case WMIDB_STORAGE_REFERENCE: // Reference or object
|
||
|
if (bOnImage)
|
||
|
{
|
||
|
hr = ReadImageValue(pIRowset, 5, &pRows, &pBuffer, dwLen);
|
||
|
if (dwCIMType == CIM_REFERENCE)
|
||
|
SetVariant(CIM_STRING, &vValue, pBuffer, dwCIMType);
|
||
|
}
|
||
|
else
|
||
|
SetVariant(CIM_STRING, &vValue, &pRowValues[pDBBindings[4].obValue], dwCIMType);
|
||
|
break;
|
||
|
case WMIDB_STORAGE_IMAGE: // Used different procedure.
|
||
|
hr = ReadImageValue(pIRowset, 5, &pRows, &pBuffer, dwLen);
|
||
|
// Read the buffer, and attempt to set it as a safearray.
|
||
|
|
||
|
if (dwCIMType != CIM_OBJECT)
|
||
|
{
|
||
|
if (SUCCEEDED(hr) && dwLen > 0)
|
||
|
{
|
||
|
long why[1];
|
||
|
unsigned char t;
|
||
|
SAFEARRAYBOUND aBounds[1];
|
||
|
aBounds[0].cElements = dwLen; // This *should* be the max value!!!!
|
||
|
aBounds[0].lLbound = 0;
|
||
|
SAFEARRAY* pArray = SafeArrayCreate(VT_UI1, 1, aBounds);
|
||
|
vValue.vt = VT_I1;
|
||
|
for (int i2 = 0; i2 < dwLen; i2++)
|
||
|
{
|
||
|
why[0] = i2;
|
||
|
t = pBuffer[i2];
|
||
|
hr = SafeArrayPutElement(pArray, why, &t);
|
||
|
}
|
||
|
vValue.vt = VT_ARRAY|VT_UI1;
|
||
|
V_ARRAY(&vValue) = pArray;
|
||
|
CWin32DefaultArena::WbemMemFree(pBuffer);
|
||
|
dwCIMType |= CIM_FLAG_ARRAY;
|
||
|
}
|
||
|
else
|
||
|
vValue.vt = VT_NULL;
|
||
|
}
|
||
|
|
||
|
break;
|
||
|
default:
|
||
|
|
||
|
hr = WBEM_E_NOT_AVAILABLE;
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
// Qualifier's property ID, if applicable.
|
||
|
if (dwPropFlags & REPDRVR_FLAG_QUALIFIER ||
|
||
|
dwPropFlags & REPDRVR_FLAG_IN_PARAM ||
|
||
|
dwPropFlags & REPDRVR_FLAG_OUT_PARAM)
|
||
|
{
|
||
|
if (pDBBindings[8].wType == DBTYPE_NUMERIC)
|
||
|
{
|
||
|
DB_NUMERIC *pTemp = (DB_NUMERIC *)&pRowValues[pDBBindings[8].obValue];
|
||
|
dRefID = CSQLExecute::GetInt64(pTemp);
|
||
|
}
|
||
|
else
|
||
|
dRefID = *((long *)&pRowValues[pDBBindings[8].obValue]);
|
||
|
}
|
||
|
|
||
|
// If this is an object (not a reference),
|
||
|
// then we need to Get the object and set it in
|
||
|
// the variant. Otherwise, the variant is simply
|
||
|
// the string path to the object.
|
||
|
// ===============================================
|
||
|
|
||
|
if (dwCIMType == CIM_OBJECT)
|
||
|
{
|
||
|
if (!bOnImage)
|
||
|
{
|
||
|
IWmiDbHandle *pHand = NULL;
|
||
|
|
||
|
hr = pSession->GetObject_Internal((LPWSTR)vValue.bstrVal, 0, WMIDB_HANDLE_TYPE_COOKIE, NULL, &pHand);
|
||
|
CReleaseMe r4 (pHand);
|
||
|
if (SUCCEEDED(hr))
|
||
|
{
|
||
|
IWbemClassObject *pEmbed = NULL;
|
||
|
hr = pHand->QueryInterface(IID_IWbemClassObject, (void **)&pEmbed);
|
||
|
if (SUCCEEDED(hr))
|
||
|
{
|
||
|
VariantClear(&vValue);
|
||
|
V_UNKNOWN(&vValue) = (IUnknown *)pEmbed;
|
||
|
vValue.vt = VT_UNKNOWN;
|
||
|
// VariantClear will release this, right?
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
IWbemClassObject *pEmbedClass = NULL, *pEmbed = NULL;
|
||
|
_IWmiObject *pInt = NULL;
|
||
|
hr = CoCreateInstance(CLSID_WbemClassObject, NULL, CLSCTX_INPROC_SERVER,
|
||
|
IID_IWbemClassObject, (void **)&pEmbedClass);
|
||
|
if (SUCCEEDED(hr))
|
||
|
{
|
||
|
VARIANT v;
|
||
|
VariantInit(&v);
|
||
|
CClearMe c (&v);
|
||
|
v.bstrVal = SysAllocString(L"Z");
|
||
|
v.vt = VT_BSTR;
|
||
|
pEmbedClass->Put(L"__Class", 0, &v, CIM_STRING);
|
||
|
hr = pEmbedClass->SpawnInstance(0, &pEmbed);
|
||
|
CReleaseMe r1 (pEmbedClass);
|
||
|
|
||
|
if (SUCCEEDED(hr))
|
||
|
{
|
||
|
hr = pEmbed->QueryInterface(IID__IWmiObject, (void **)&pInt);
|
||
|
CReleaseMe r (pInt);
|
||
|
if (SUCCEEDED(hr))
|
||
|
{
|
||
|
LPVOID pTaskMem = NULL;
|
||
|
|
||
|
if (SUCCEEDED(hr))
|
||
|
{
|
||
|
pTaskMem = CoTaskMemAlloc( dwLen );
|
||
|
|
||
|
if ( NULL != pTaskMem )
|
||
|
{
|
||
|
// Copy the memory
|
||
|
CopyMemory( pTaskMem, pBuffer, dwLen );
|
||
|
hr = pInt->SetObjectMemory(pTaskMem, dwLen);
|
||
|
if (SUCCEEDED(hr))
|
||
|
{
|
||
|
VariantClear(&vValue);
|
||
|
V_UNKNOWN(&vValue) = (IUnknown *)pEmbed;
|
||
|
vValue.vt = VT_UNKNOWN;
|
||
|
dwStorage = WMIDB_STORAGE_REFERENCE;
|
||
|
}
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
hr = WBEM_E_OUT_OF_MEMORY;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// For array properties, what we actually need to do is
|
||
|
// see if the property exists already. If not, we
|
||
|
// initialize the safe array and add the first element.
|
||
|
// If so, we simply set the value in the existing array.
|
||
|
// ======================================================
|
||
|
|
||
|
if (FAILED(hr))
|
||
|
break;
|
||
|
|
||
|
if (dwPropFlags & REPDRVR_FLAG_ARRAY && (dwStorage != WMIDB_STORAGE_IMAGE) )
|
||
|
{
|
||
|
dwCIMType |= CIM_FLAG_ARRAY;
|
||
|
|
||
|
VARIANT vTemp;
|
||
|
CClearMe c (&vTemp);
|
||
|
VariantCopy(&vTemp, &vValue);
|
||
|
long lTemp;
|
||
|
CIMTYPE cimtype;
|
||
|
VariantClear(&vValue);
|
||
|
if (SUCCEEDED(pNewObj->Get(sPropName, 0, &vValue, &cimtype, &lTemp)))
|
||
|
{
|
||
|
if (PropIds[dwPropertyId])
|
||
|
{
|
||
|
SAFEARRAY *pArray = V_ARRAY(&vValue);
|
||
|
hr = PutVariantInArray(&pArray, dwPosition, &vTemp);
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
if (vValue.vt != VT_NULL)
|
||
|
VariantClear(&vValue);
|
||
|
|
||
|
// This is a new object.
|
||
|
SAFEARRAYBOUND aBounds[1];
|
||
|
aBounds[0].cElements = dwPosition+1; // This *should* be the max value!!!!
|
||
|
aBounds[0].lLbound = 0;
|
||
|
SAFEARRAY* pArray = SafeArrayCreate(vTemp.vt, 1, aBounds);
|
||
|
hr = PutVariantInArray(&pArray, dwPosition, &vTemp);
|
||
|
vValue.vt = VT_ARRAY|vTemp.vt;
|
||
|
V_ARRAY(&vValue) = pArray;
|
||
|
PropIds[dwPropertyId] = true;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (!(dwPropFlags & REPDRVR_FLAG_QUALIFIER) &&
|
||
|
!(dwPropFlags & REPDRVR_FLAG_IN_PARAM) &&
|
||
|
!(dwPropFlags & REPDRVR_FLAG_OUT_PARAM))
|
||
|
{
|
||
|
hr = pNewObj->Put(sPropName, 0, &vValue, dwCIMType);
|
||
|
}
|
||
|
|
||
|
// If this is a qualifier on a class, get the qualifier set and set the value.
|
||
|
else if (dwPropFlags & REPDRVR_FLAG_QUALIFIER )
|
||
|
{
|
||
|
if (dRefID != 0)
|
||
|
{
|
||
|
_bstr_t sProp2;
|
||
|
DWORD dwFlags2, dwRefID;
|
||
|
|
||
|
hr = pSchema->GetPropertyInfo (dRefID, &sProp2, NULL, NULL,
|
||
|
NULL, &dwFlags2, NULL, NULL, &dwRefID);
|
||
|
if (SUCCEEDED(hr))
|
||
|
{
|
||
|
IWbemQualifierSet *pQS = NULL;
|
||
|
hr = pNewObj->GetPropertyQualifierSet(sProp2, &pQS);
|
||
|
CReleaseMe r4 (pQS);
|
||
|
if (SUCCEEDED(hr))
|
||
|
pQS->Put(sPropName, &vValue, dwFlags);
|
||
|
}
|
||
|
}
|
||
|
else // Its just a class/instance qualifier. Set it.
|
||
|
{
|
||
|
IWbemQualifierSet *pQS = NULL;
|
||
|
hr = pNewObj->GetQualifierSet(&pQS);
|
||
|
CReleaseMe r5 (pQS);
|
||
|
if (SUCCEEDED(hr))
|
||
|
hr = pQS->Put(sPropName, &vValue, dwFlags);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
}
|
||
|
}
|
||
|
else
|
||
|
hr = WBEM_S_NO_MORE_DATA;
|
||
|
}
|
||
|
|
||
|
pIRowset->ReleaseRows(cRowsObtained, rghRows, NULL, NULL, NULL);
|
||
|
|
||
|
pIAccessor->ReleaseAccessor(hAccessor, NULL);
|
||
|
|
||
|
pMalloc->Free( pColumnsInfo );
|
||
|
pMalloc->Free( pColumnStrings );
|
||
|
|
||
|
if (hr == DB_S_ENDOFROWSET)
|
||
|
hr = WBEM_S_NO_MORE_DATA;
|
||
|
|
||
|
return hr;
|
||
|
|
||
|
}
|
||
|
|