Mercurial > projects > dwt-win
view dwt/ole/win32/OleAutomation.d @ 98:ecba636b634e
package dwt/ole
author | Frank Benoit <benoit@tionex.de> |
---|---|
date | Sat, 09 Feb 2008 03:36:27 +0100 |
parents | 2e6eb5bd2c16 |
children | ab60f3309436 |
line wrap: on
line source
/******************************************************************************* * Copyright (c) 2000, 2007 IBM Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * IBM Corporation - initial API and implementation * Port to the D programming language: * Frank Benoit <benoit@tionex.de> *******************************************************************************/ module dwt.ole.win32.OleAutomation; import dwt.internal.ole.win32.COM; import dwt.internal.ole.win32.COMTYPES; import dwt.internal.ole.win32.OAIDL; // import dwt.internal.ole.win32.DISPPARAMS; // import dwt.internal.ole.win32.EXCEPINFO; // import dwt.internal.ole.win32.FUNCDESC; // import dwt.internal.ole.win32.GUID; // import dwt.internal.ole.win32.IDispatch; // import dwt.internal.ole.win32.ITypeInfo; // import dwt.internal.ole.win32.TYPEATTR; // import dwt.internal.ole.win32.VARDESC; import dwt.internal.win32.OS; import dwt.ole.win32.OleClientSite; import dwt.ole.win32.OlePropertyDescription; import dwt.ole.win32.OleFunctionDescription; import dwt.ole.win32.OleParameterDescription; import dwt.ole.win32.Variant; import dwt.ole.win32.OLE; import dwt.dwthelper.utils; /** * OleAutomation provides a generic mechanism for accessing functionality that is * specific to a particular ActiveX Control or OLE Document. * * <p>The OLE Document or ActiveX Control must support the IDispatch interface in order to provide * OleAutomation support. The additional functionality provided by the OLE Object is specified in * its IDL file. The additional methods can either be to get property values (<code>getProperty</code>), * to set property values (<code>setProperty</code>) or to invoke a method (<code>invoke</code> or * <code>invokeNoReply</code>). Arguments are passed around in the form of <code>Variant</code> * objects. * * <p>Here is a sample IDL fragment: * * <pre> * interface IMyControl : IDispatch * { * [propget, id(0)] HRESULT maxFileCount([retval, out] int *c); * [propput, id(0)] HRESULT maxFileCount([in] int c); * [id(1)] HRESULT AddFile([in] BSTR fileName); * }; * </pre> * * <p>An example of how to interact with this extended functionality is shown below: * * <code><pre> * OleAutomation automation = new OleAutomation(myControlSite); * * // Look up the ID of the maxFileCount parameter * int[] rgdispid = automation.getIDsOfNames(new String[]{"maxFileCount"}); * int maxFileCountID = rgdispid[0]; * * // Set the property maxFileCount to 100: * if (automation.setProperty(maxFileCountID, new Variant(100))) { * System.out.println("Max File Count was successfully set."); * } * * // Get the new value of the maxFileCount parameter: * Variant pVarResult = automation.getProperty(maxFileCountID); * if (pVarResult !is null) { * System.out.println("Max File Count is "+pVarResult.getInt()); * } * * // Invoke the AddFile method * // Look up the IDs of the AddFile method and its parameter * rgdispid = automation.getIDsOfNames(new String[]{"AddFile", "fileName"}); * int dispIdMember = rgdispid[0]; * int[] rgdispidNamedArgs = new int[] {rgdispid[1]}; * * // Convert arguments to Variant objects * Variant[] rgvarg = new Variant[1]; * String fileName = "C:\\testfile"; * rgvarg[0] = new Variant(fileName); * * // Call the method * Variant pVarResult = automation.invoke(dispIdMember, rgvarg, rgdispidNamedArgs); * * // Check the return value * if (pVarResult is null || pVarResult.getInt() !is OLE.S_OK){ * System.out.println("Failed to add file "+fileName); * } * * automation.dispose(); * * </pre></code> */ public final class OleAutomation { private IDispatch objIDispatch; private char[] exceptionDescription; private ITypeInfo objITypeInfo; this(IDispatch idispatch) { if (idispatch is null) OLE.error(OLE.ERROR_INVALID_INTERFACE_ADDRESS); objIDispatch = idispatch; objIDispatch.AddRef(); int result = objIDispatch.GetTypeInfo(0, COM.LOCALE_USER_DEFAULT, &objITypeInfo); if (result is OLE.S_OK) { objITypeInfo.AddRef(); } } /** * Creates an OleAutomation object for the specified client. * * @param clientSite the site for the OLE Document or ActiveX Control whose additional functionality * you need to access * * @exception IllegalArgumentException <ul> * <li>ERROR_INVALID_INTERFACE_ADDRESS when called with an invalid client site * </ul> */ public this(OleClientSite clientSite) { if (clientSite is null) OLE.error(OLE.ERROR_INVALID_INTERFACE_ADDRESS); objIDispatch = clientSite.getAutomationObject(); auto result = objIDispatch.GetTypeInfo(0, COM.LOCALE_USER_DEFAULT, &objITypeInfo); if (result is OLE.S_OK) { objITypeInfo.AddRef(); } } /** * Disposes the automation object. * <p> * This method releases the IDispatch interface on the OLE Document or ActiveX Control. * Do not use the OleAutomation object after it has been disposed. */ public void dispose() { if (objIDispatch !is null){ objIDispatch.Release(); } objIDispatch = null; if (objITypeInfo !is null){ objITypeInfo.Release(); } objITypeInfo = null; } IDispatch getAddress() { return objIDispatch; } public char[] getHelpFile(int dispId) { if (objITypeInfo is null) return null; BSTR file; HRESULT rc = objITypeInfo.GetDocumentation(dispId, null, null, null, &file ); if (rc is OLE.S_OK) { char[] str = WCHARzToStr( file, -1 ); COM.SysFreeString(file); return str; } return null; } public char[] getDocumentation(int dispId) { if (objITypeInfo is null) return null; BSTR doc; HRESULT rc = objITypeInfo.GetDocumentation(dispId, null, &doc, null, null ); if (rc == OLE.S_OK) { char[] s = WCHARzToStr(doc, -1); COM.SysFreeString(doc); return s; } return null; } public OlePropertyDescription getPropertyDescription(int index) { if (objITypeInfo is null) return null; VARDESC* vardesc; HRESULT rc = objITypeInfo.GetVarDesc(index, &vardesc); if (rc != OLE.S_OK) return null; // VARDESC* vardesc = new VARDESC(); // COM.MoveMemory(vardesc, ppVarDesc[0], VARDESC.sizeof); OlePropertyDescription data = new OlePropertyDescription(); data.id = vardesc.memid; data.name = getName(vardesc.memid); data.type = vardesc.elemdescVar.tdesc.vt; if (data.type == OLE.VT_PTR) { // short[] vt = new short[1]; // COM.MoveMemory(vt, vardesc.elemdescVar.tdesc_union + 4, 2); // TODO: data.type = vardesc.elemdescVar.tdesc.vt; } data.flags = vardesc.wVarFlags; data.kind = vardesc.varkind; data.description = getDocumentation(vardesc.memid); data.helpFile = getHelpFile(vardesc.memid); objITypeInfo.ReleaseVarDesc(vardesc); return data; } public OleFunctionDescription getFunctionDescription(int index) { if (objITypeInfo is null) return null; FUNCDESC* funcdesc; HRESULT rc = objITypeInfo.GetFuncDesc(index, &funcdesc); if (rc != OLE.S_OK) return null; OleFunctionDescription data = new OleFunctionDescription(); data.id = funcdesc.memid; data.optionalArgCount = funcdesc.cParamsOpt; data.invokeKind = funcdesc.invkind; data.funcKind = funcdesc.funckind; data.flags = funcdesc.wFuncFlags; data.callingConvention = funcdesc.callconv; data.documentation = getDocumentation(funcdesc.memid); data.helpFile = getHelpFile(funcdesc.memid); char[][] names = getNames(funcdesc.memid, funcdesc.cParams + 1); if (names.length > 0) { data.name = names[0]; } data.args = new OleParameterDescription[funcdesc.cParams]; for (int i = 0; i < data.args.length; i++) { data.args[i] = new OleParameterDescription(); if (names.length > i + 1) { data.args[i].name = names[i + 1]; } short[1] vt; COM.MoveMemory(vt.ptr, funcdesc.lprgelemdescParam + i * 16 + 4, 2); if (vt[0] is OLE.VT_PTR) { int[1] pTypedesc; COM.MoveMemory(pTypedesc.ptr, funcdesc.lprgelemdescParam + i * 16, 4); short[1] vt2; COM.MoveMemory(vt2.ptr, pTypedesc[0] + 4, 2); vt[0] = cast(short)(vt2[0] | COM.VT_BYREF); } data.args[i].type = vt[0]; short[1] wParamFlags; COM.MoveMemory(wParamFlags.ptr, funcdesc.lprgelemdescParam + i * 16 + 12, 2); data.args[i].flags = wParamFlags[0]; } data.returnType = funcdesc.elemdescFunc.tdesc.vt; if (data.returnType is OLE.VT_PTR) { ushort[1] vt; COM.MoveMemory(vt.ptr, funcdesc.elemdescFunc.tdesc.u.lpadesc, 2); data.returnType = vt[0]; } objITypeInfo.ReleaseFuncDesc(funcdesc); return data; } public TYPEATTR* getTypeInfoAttributes() { if (objITypeInfo is null) return null; TYPEATTR* ppTypeAttr; HRESULT rc = objITypeInfo.GetTypeAttr(&ppTypeAttr); if (rc !is OLE.S_OK) return null; TYPEATTR* typeattr = new TYPEATTR(); COM.MoveMemory(typeattr, ppTypeAttr, TYPEATTR.sizeof); objITypeInfo.ReleaseTypeAttr(ppTypeAttr); return typeattr; } public char[] getName(int dispId) { if (objITypeInfo is null) return null; BSTR name; HRESULT rc = objITypeInfo.GetDocumentation(dispId, &name, null, null, null ); if (rc == OLE.S_OK) { char[] s = WCHARzToStr(name, -1); COM.SysFreeString(name); return s; } return null; } public char[][] getNames(int dispId, int maxSize) { if (objITypeInfo is null) return new char[][0]; BSTR[] names = new BSTR[maxSize]; uint count; HRESULT rc = objITypeInfo.GetNames(dispId, names.ptr, maxSize, &count); if (rc == OLE.S_OK) { char[][] newNames = new char[][count]; for(int i=0; i<count; ++i){ newNames[i] = WCHARzToStr(names[i], -1); COM.SysFreeString(names[i]); } return newNames; } return null; } /** * Returns the positive integer values (IDs) that are associated with the specified names by the * IDispatch implementor. If you are trying to get the names of the parameters in a method, the first * String in the names array must be the name of the method followed by the names of the parameters. * * @param names an array of names for which you require the identifiers * * @return positive integer values that are associated with the specified names in the same * order as the names where provided; or null if the names are unknown */ public int[] getIDsOfNames(char[][] names) { int count = names.length; wchar*[] wcNames = new wchar*[count]; for(int i=0; i<count; ++i){ wcNames[i] = StrToWCHARz(names[i]); } int[] rgdispid = new int[count]; // TODO: NULL GUID ?? GUID id; HRESULT result = objIDispatch.GetIDsOfNames(&id, wcNames.ptr, count, COM.LOCALE_USER_DEFAULT, rgdispid.ptr); if (result != COM.S_OK) return null; return rgdispid; } /** * Returns a description of the last error encountered. * * @return a description of the last error encountered */ public char[] getLastError() { return exceptionDescription; } /** * Returns the value of the property specified by the dispIdMember. * * @param dispIdMember the ID of the property as specified by the IDL of the ActiveX Control; the * value for the ID can be obtained using OleAutomation.getIDsOfNames * * @return the value of the property specified by the dispIdMember or null */ public Variant getProperty(int dispIdMember) { Variant pVarResult = new Variant(); HRESULT result = invoke(dispIdMember, COM.DISPATCH_PROPERTYGET, null, null, pVarResult); return (result is OLE.S_OK) ? pVarResult : null; } /** * Returns the value of the property specified by the dispIdMember. * * @param dispIdMember the ID of the property as specified by the IDL of the ActiveX Control; the * value for the ID can be obtained using OleAutomation.getIDsOfNames * * @param rgvarg an array of arguments for the method. All arguments are considered to be * read only unless the Variant is a By Reference Variant type. * * @return the value of the property specified by the dispIdMember or null * * @since 2.0 */ public Variant getProperty(int dispIdMember, Variant[] rgvarg) { Variant pVarResult = new Variant(); HRESULT result = invoke(dispIdMember, COM.DISPATCH_PROPERTYGET, rgvarg, null, pVarResult); return (result is OLE.S_OK) ? pVarResult : null; } /** * Returns the value of the property specified by the dispIdMember. * * @param dispIdMember the ID of the property as specified by the IDL of the ActiveX Control; the * value for the ID can be obtained using OleAutomation.getIDsOfNames * * @param rgvarg an array of arguments for the method. All arguments are considered to be * read only unless the Variant is a By Reference Variant type. * * @param rgdispidNamedArgs an array of identifiers for the arguments specified in rgvarg; the * parameter IDs must be in the same order as their corresponding values; * all arguments must have an identifier - identifiers can be obtained using * OleAutomation.getIDsOfNames * * @return the value of the property specified by the dispIdMember or null * * @since 2.0 */ public Variant getProperty(int dispIdMember, Variant[] rgvarg, int[] rgdispidNamedArgs) { Variant pVarResult = new Variant(); HRESULT result = invoke(dispIdMember, COM.DISPATCH_PROPERTYGET, rgvarg, rgdispidNamedArgs, pVarResult); return (result is OLE.S_OK) ? pVarResult : null; } /** * Invokes a method on the OLE Object; the method has no parameters. * * @param dispIdMember the ID of the method as specified by the IDL of the ActiveX Control; the * value for the ID can be obtained using OleAutomation.getIDsOfNames * * @return the result of the method or null if the method failed to give result information */ public Variant invoke(int dispIdMember) { Variant pVarResult = new Variant(); HRESULT result = invoke(dispIdMember, COM.DISPATCH_METHOD, null, null, pVarResult); return (result is COM.S_OK) ? pVarResult : null; } /** * Invokes a method on the OLE Object; the method has no optional parameters. * * @param dispIdMember the ID of the method as specified by the IDL of the ActiveX Control; the * value for the ID can be obtained using OleAutomation.getIDsOfNames * * @param rgvarg an array of arguments for the method. All arguments are considered to be * read only unless the Variant is a By Reference Variant type. * * @return the result of the method or null if the method failed to give result information */ public Variant invoke(int dispIdMember, Variant[] rgvarg) { Variant pVarResult = new Variant(); HRESULT result = invoke(dispIdMember, COM.DISPATCH_METHOD, rgvarg, null, pVarResult); return (result is COM.S_OK) ? pVarResult : null; } /** * Invokes a method on the OLE Object; the method has optional parameters. It is not * necessary to specify all the optional parameters, only include the parameters for which * you are providing values. * * @param dispIdMember the ID of the method as specified by the IDL of the ActiveX Control; the * value for the ID can be obtained using OleAutomation.getIDsOfNames * * @param rgvarg an array of arguments for the method. All arguments are considered to be * read only unless the Variant is a By Reference Variant type. * * @param rgdispidNamedArgs an array of identifiers for the arguments specified in rgvarg; the * parameter IDs must be in the same order as their corresponding values; * all arguments must have an identifier - identifiers can be obtained using * OleAutomation.getIDsOfNames * * @return the result of the method or null if the method failed to give result information */ public Variant invoke(int dispIdMember, Variant[] rgvarg, int[] rgdispidNamedArgs) { Variant pVarResult = new Variant(); HRESULT result = invoke(dispIdMember, COM.DISPATCH_METHOD, rgvarg, rgdispidNamedArgs, pVarResult); return (result is COM.S_OK) ? pVarResult : null; } private int invoke(int dispIdMember, ushort wFlags, Variant[] rgvarg, int[] rgdispidNamedArgs, Variant pVarResult) { assert(objIDispatch); // get the IDispatch interface for the control if (objIDispatch is null) return COM.E_FAIL; // create a DISPPARAMS structure for the input parameters DISPPARAMS pDispParams; // store arguments in rgvarg if (rgvarg !is null && rgvarg.length > 0) { VARIANT[] tempArgs = new VARIANT[rgvarg.length]; for (int i = 0; i < rgvarg.length ; ++i) { rgvarg[i].getData(&tempArgs[i]); } // the reverse sequency tempArgs.reverse; pDispParams.cArgs = tempArgs.length; pDispParams.rgvarg = tempArgs.ptr; } // if arguments have ids, store the ids in rgdispidNamedArgs if (rgdispidNamedArgs !is null && rgdispidNamedArgs.length > 0) { DISPID[] tempArgs = rgdispidNamedArgs.dup; // the reverse sequency tempArgs.reverse; pDispParams.cNamedArgs = tempArgs.length; pDispParams.rgdispidNamedArgs = tempArgs.ptr; } // invoke the method EXCEPINFO excepInfo; uint pArgErr; VARIANT* pVarResultAddress = null; if (pVarResult !is null) pVarResultAddress = new VARIANT(); GUID id; // IID_NULL /* HRESULT Invoke( [in] DISPID dispIdMember, [in] REFIID riid, [in] LCID lcid, [in] WORD wFlags, [in, out] DISPPARAMS * pDispParams, [out] VARIANT * pVarResult, [out] EXCEPINFO * pExcepInfo, [out] UINT * puArgErr ); */ HRESULT result = objIDispatch.Invoke(dispIdMember, &id, COM.LOCALE_USER_DEFAULT, wFlags, &pDispParams, pVarResultAddress, &excepInfo, &pArgErr); if (pVarResultAddress !is null){ pVarResult.setData(pVarResultAddress); COM.VariantClear(pVarResultAddress); } // free the Dispparams resources if (pDispParams.rgvarg !is null) { for (int i = 0, length = rgvarg.length; i < length; i++){ COM.VariantClear(&pDispParams.rgvarg[i]); } pDispParams.rgvarg = null; } pDispParams.rgdispidNamedArgs = null; // save error string and cleanup EXCEPINFO manageExcepinfo(result, &excepInfo); return result; } /** * Invokes a method on the OLE Object; the method has no parameters. In the early days of OLE, * the IDispatch interface was not well defined and some applications (mainly Word) did not support * a return value. For these applications, call this method instead of calling * <code>public void invoke(int dispIdMember)</code>. * * @param dispIdMember the ID of the method as specified by the IDL of the ActiveX Control; the * value for the ID can be obtained using OleAutomation.getIDsOfNames * * @exception dwt.DWTException <ul> * <li>ERROR_ACTION_NOT_PERFORMED when method invocation fails * </ul> */ public void invokeNoReply(int dispIdMember) { HRESULT result = invoke(dispIdMember, COM.DISPATCH_METHOD, null, null, null); if (result !is COM.S_OK) OLE.error(__FILE__, __LINE__, OLE.ERROR_ACTION_NOT_PERFORMED, result); } /** * Invokes a method on the OLE Object; the method has no optional parameters. In the early days of OLE, * the IDispatch interface was not well defined and some applications (mainly Word) did not support * a return value. For these applications, call this method instead of calling * <code>public void invoke(int dispIdMember, Variant[] rgvarg)</code>. * * @param dispIdMember the ID of the method as specified by the IDL of the ActiveX Control; the * value for the ID can be obtained using OleAutomation.getIDsOfNames * * @param rgvarg an array of arguments for the method. All arguments are considered to be * read only unless the Variant is a By Reference Variant type. * * @exception dwt.DWTException <ul> * <li>ERROR_ACTION_NOT_PERFORMED when method invocation fails * </ul> */ public void invokeNoReply(int dispIdMember, Variant[] rgvarg) { int result = invoke(dispIdMember, COM.DISPATCH_METHOD, rgvarg, null, null); if (result !is COM.S_OK) OLE.error(__FILE__, __LINE__, OLE.ERROR_ACTION_NOT_PERFORMED, result); } /** * Invokes a method on the OLE Object; the method has optional parameters. It is not * necessary to specify all the optional parameters, only include the parameters for which * you are providing values. In the early days of OLE, the IDispatch interface was not well * defined and some applications (mainly Word) did not support a return value. For these * applications, call this method instead of calling * <code>public void invoke(int dispIdMember, Variant[] rgvarg, int[] rgdispidNamedArgs)</code>. * * @param dispIdMember the ID of the method as specified by the IDL of the ActiveX Control; the * value for the ID can be obtained using OleAutomation.getIDsOfNames * * @param rgvarg an array of arguments for the method. All arguments are considered to be * read only unless the Variant is a By Reference Variant type. * * @param rgdispidNamedArgs an array of identifiers for the arguments specified in rgvarg; the * parameter IDs must be in the same order as their corresponding values; * all arguments must have an identifier - identifiers can be obtained using * OleAutomation.getIDsOfNames * * @exception dwt.DWTException <ul> * <li>ERROR_ACTION_NOT_PERFORMED when method invocation fails * </ul> */ public void invokeNoReply(int dispIdMember, Variant[] rgvarg, int[] rgdispidNamedArgs) { HRESULT result = invoke(dispIdMember, COM.DISPATCH_METHOD, rgvarg, rgdispidNamedArgs, null); if (result !is COM.S_OK) OLE.error(__FILE__, __LINE__, OLE.ERROR_ACTION_NOT_PERFORMED, result); } private void manageExcepinfo(int hResult, EXCEPINFO* excepInfo) { if (hResult is COM.S_OK){ exceptionDescription = "No Error"; //$NON-NLS-1$ return; } // extract exception info if (hResult is COM.DISP_E_EXCEPTION) { if (excepInfo.bstrDescription !is null){ exceptionDescription = WCHARzToStr(excepInfo.bstrDescription); } else { exceptionDescription = ("OLE Automation Error Exception "); //$NON-NLS-1$ if (excepInfo.wCode != 0){ exceptionDescription ~= "code = "; exceptionDescription ~= cast(int)(excepInfo.wCode); //$NON-NLS-1$ } else if (excepInfo.scode != 0){ exceptionDescription ~= "code = "; exceptionDescription ~= (excepInfo.scode); //$NON-NLS-1$ } } } else { exceptionDescription = ("OLE Automation Error HResult : ") ~ toHex(hResult); //$NON-NLS-1$ } // cleanup EXCEPINFO struct if (excepInfo.bstrDescription !is null) COM.SysFreeString(excepInfo.bstrDescription); if (excepInfo.bstrHelpFile !is null) COM.SysFreeString(excepInfo.bstrHelpFile); if (excepInfo.bstrSource !is null) COM.SysFreeString(excepInfo.bstrSource); } /** * Sets the property specified by the dispIdMember to a new value. * * @param dispIdMember the ID of the property as specified by the IDL of the ActiveX Control; the * value for the ID can be obtained using OleAutomation.getIDsOfNames * @param rgvarg the new value of the property * * @return true if the operation was successful */ public bool setProperty(int dispIdMember, Variant rgvarg) { Variant[] rgvarg2 = new Variant[1]; rgvarg2[0] = rgvarg; int[] rgdispidNamedArgs; rgdispidNamedArgs ~= COM.DISPID_PROPERTYPUT; ushort dwFlags = COM.DISPATCH_PROPERTYPUT; if ((rgvarg.getType() & COM.VT_BYREF) == COM.VT_BYREF) dwFlags = COM.DISPATCH_PROPERTYPUTREF; Variant pVarResult = new Variant(); HRESULT result = invoke(dispIdMember, dwFlags, rgvarg2, rgdispidNamedArgs, pVarResult); return (result == COM.S_OK); } /** * Sets the property specified by the dispIdMember to a new value. * * @param dispIdMember the ID of the property as specified by the IDL of the ActiveX Control; the * value for the ID can be obtained using OleAutomation.getIDsOfNames * @param rgvarg an array of arguments for the method. All arguments are considered to be * read only unless the Variant is a By Reference Variant type. * * @return true if the operation was successful * * @since 2.0 */ public bool setProperty(int dispIdMember, Variant[] rgvarg) { int[] rgdispidNamedArgs; rgdispidNamedArgs ~= COM.DISPID_PROPERTYPUT; ushort dwFlags = COM.DISPATCH_PROPERTYPUT; for (int i = 0; i < rgvarg.length; i++) { if ((rgvarg[i].getType() & COM.VT_BYREF) == COM.VT_BYREF) dwFlags = COM.DISPATCH_PROPERTYPUTREF; } Variant pVarResult = new Variant(); HRESULT result = invoke(dispIdMember, dwFlags, rgvarg, rgdispidNamedArgs, pVarResult); return (result == COM.S_OK); } }