Array_Of_Int in SOAP client - delphi

I have a very interesting issue when I call a SOAP method with my client, I must pass a parameter which is of type Array_Of_Int(Array_Of_Int = array of Integer), the problem is that when the array is being generated in the request, it generates the following
<ArrayParam>
<item>12345</item>
<item>23456</item>
<item>34567</item>
</ArrayParam>
but I believe the server expects
<ArrayParam>12345</ArrayParam>
<ArrayParam>23456</ArrayParam>
<ArrayParam>34567</ArrayParam>
I'm pretty sure that Delphi has a workaround for this issue somehow in the RegisterSerializeOptions or RegisterInvokeOptions however I can't seem to find the issue, thoughts?!
Thank you all for your time, I'm using Delphi 2010.
EDIT: in order to fix this issue, as Bruneau mentioned, we need to have the following code added in the initialization section of generated .pas file:
InvRegistry.RegisterInvokeOptions(TypeInfo(<ServerInterfaceNameHere>), ioDocument);
However that imposes another issue, the namespace, as a quick and pretty elegant fix, I've added the following code in the THTTPRio's OnBeforeExecute method
procedure TMyDataModule.MyRioBeforeExecute(const MethodName: string; SOAPRequest: TStream);
procedure FixNamespaces;
var
LStrings: TStringList;
begin
LStrings := TStringList.Create;
try
SOAPRequest.Position := 0;
LStrings.LoadFromStream(SOAPRequest);
SOAPRequest.Position := 0;
SOAPRequest.Size := 0;
LStrings.Text := StringReplace(LStrings.Text, MethodName, 'NS1:' + MethodName, [rfReplaceAll]);
LStrings.Text := StringReplace(LStrings.Text, MethodName + ' xmlns', MethodName + ' xmlns:NS1', []);
LStrings.SaveToStream(SOAPRequest);
SOAPRequest.Position := 0;
finally
FreeAndNil(LStrings);
end; // tryf
end; // procedure FixNamespaces;
begin
FixNamespaces;
end;
The above is just a fix, I really hope I can find a much cleaner and elegant solution to this issue, if anyone knows, please DO post your answer.

The two serialization options you described are both valid and needed. The issue is that from a language/native point of view, Delphi represents both of them with a dynamic array (Array_Of_Int = array of Integer). So, the runtime must be told whether to serialize for a "Pure Collection" (the first kind with elements and the outer ArrayParam) or "unbounded elements" ("ArrayParam" elements).
In earlier versions, including 2010, you could instruct the runtime to serialize as unbounded elements with the following registration:
RemClassRegistry.RegisterSerializeOptions(TypeInfo(Array_Of_Int), [xoInlineArrays]);
If the type is used in a property, you could also simply tag the property itself as unbounded, as in:
property propName: Array_Of_Int Index (IS_UNBD) read FName write FName;
The drawback of the registration approach is that it does not allow one to (re)use the type for both serializations. In Delphi XE this was remedied and now the type is never registered for a particular scheme. Instead each Dynamic Array property or parameter specifies whether it's a "Pure Collection" vs. "Unbounded Element", eliminating the need to have distinct Dynamic Array of Integers for each serialization.
Cheers,
Bruneau

Since no one cares to post their answer or have no other idea on how to fix this issue, I'll just post my fix until others can come out with a more elegant solution than editing the request.
Make sure the next line of code is added in the initialization section of the *.pas file generated when you imported the WSDL file(big thanks to Bruneau for pointing this out)
InvRegistry.RegisterInvokeOptions(TypeInfo(<ServerInterfaceNameHere>), ioDocument);
However that imposes another issue, the namespace, as a quick and pretty elegant fix, I've added the following code in the THTTPRio's OnBeforeExecute method
procedure TMyDataModule.MyRioBeforeExecute(const MethodName: string; SOAPRequest: TStream);
procedure FixNamespaces;
var
LStrings: TStringList;
begin
LStrings := TStringList.Create;
try
SOAPRequest.Position := 0;
LStrings.LoadFromStream(SOAPRequest);
SOAPRequest.Position := 0;
SOAPRequest.Size := 0;
LStrings.Text := StringReplace(LStrings.Text, MethodName, 'NS1:' + MethodName, [rfReplaceAll]);
LStrings.Text := StringReplace(LStrings.Text, MethodName + ' xmlns', MethodName + ' xmlns:NS1', []);
LStrings.SaveToStream(SOAPRequest);
SOAPRequest.Position := 0;
finally
FreeAndNil(LStrings);
end; // tryf
end; // procedure FixNamespaces;
begin
FixNamespaces;
// other possible issue to be fixed -- if any
end;

Related

# signs in ADO locates (Delphi XE5)

With an TADOQuery.Locate that uses a list of fields and a VarArray of values, if one of the values contains a # sign, we get this exception:
'Arguments are of the wrong type, are out of acceptable range, or are in conflict with one another.'
I've traced this down to ADODB which itself seems to be using # signs as delimiters.
Is there a way to escape #-signs so that the query doesn't fail?
* EDIT 1 *
I was wrong. What causes this failure is a string that has a pound sign and a single quote. The code shown below fails with error message noted above.
What really worries us is that when it fails running as an .exe outside the IDE, there's no runtime exception. We only see the exception when we're in the IDE. If our programmers hadn't happened to be using data that triggers this we never would have known that the .Locate returned FALSE because of a runtime error, not because a matching record was not found.
Code:
var
SearchArray: Variant;
begin
SearchArray := VarArrayCreate([0,1], VarVariant);
SearchArray[0] := 'T#more''wo';
SearchArray[1] := 'One';
ADOQuery.Locate('FieldName1;FieldName2', SearchArray, []);
Please see Updates below; I've found a work-around that's at least worth testing.
Even with Sql Server tables, the # shouldn't need to be escaped.
The following code works correctly in D7..XE8
procedure TForm1.Button1Click(Sender: TObject);
begin
AdoQuery1.Locate('country;class', VarArrayOf(['GB', Edit1.Text]), []);
end;
when Edit1.Text contains 'D#E', so I think your problem must lie elsewhere. Try a minimalist project with just that code, after rebooting your machine.
Update: As noted in a comment, there is a problem with .Locate where the expression
passed to GetFilterStr (in ADODB.Pas) contains a # followed by a single quote. To try and
work out a work-around for this, I've transplanted GetFilterStr into my code and have
been experimenting with using it to construct a recordset filter on my AdoQuery, as I noticed
that this is what .Locate does in the statement
FLookupCursor.Filter := LocateFilter;
The code I'm using for this, including my "corrected" version of GetFilterStr, is below.
What I haven't managed to figure out yet is how to avoid getting an exception on
AdoQuery1.Recordset.Filter := S;
when the filter expression yields no records.
(Btw, for convenience, I'm doing this in D7, but using XE8's GetFilterStr, which is why I've had to comment out the reference to ftFixedWideChar)
function GetFilterStr(Field: TField; Value: Variant; Partial: Boolean = False): WideString;
// From XE8 Data.Win.ADODB
var
Operator,
FieldName,
QuoteCh: WideString;
begin
QuoteCh := '';
Operator := '=';
FieldName := Field.FieldName;
if Pos(' ', FieldName) > 0 then
FieldName := WideFormat('[%s]', [FieldName]);
if VarIsNull(Value) or VarIsClear(Value) then
Value := 'Null'
else
case Field.DataType of
ftDate, ftTime, ftDateTime:
QuoteCh := '#';
ftString, ftFixedChar, ftWideString://, ftFixedWideChar:
begin
if Partial and (Value <> '') then
begin
Value := Value + '*';
Operator := ' like '; { Do not localize }
end;
{.$define UseOriginal}
{$ifdef UseOriginal}
if Pos('''', Value) > 0 then
QuoteCh := '#' else
QuoteCh := '''';
{$else}
QuoteCh := '''';
if Pos('''', Value) > 0 then begin
QuoteCh := '';
Value := QuotedStr(Value);
end;
{$endif}
end;
end;
Result := WideFormat('(%s%s%s%s%2:s)', [FieldName, Operator, QuoteCh, VarToWideStr(Value)]);
end;
procedure TForm1.CreateFilterExpr;
var
S : String;
begin
// clear any existing filter
AdoQuery1.Recordset.Filter := adFilterNone;
AdoQuery1.Refresh;
if edFilter.Text = '' then Exit;
S := GetFilterStr(AdoQuery1.FieldByName('Applicant'), edFilter.Text, cbPartialKey.Checked);
// Add the filter expr to Memo1 so we can inspect it
Memo1.Lines.Add(S);
try
AdoQuery1.Recordset.Filter := S;
AdoQuery1.Refresh;
except
end;
end;
procedure TForm1.FilterClick(Sender: TObject);
begin
CreateFilterExpr;
end;
Update 2: Try the following:
Copy Data.Win.ADODB.Pas to your project directory
In it, replace GetFilterExpr by the version above, making sure that UseOriginal
isn't DEFINEd, and that ftFixedWideChar is reinstated in the Case statement.
Build and run your project
In XE8 at any rate, my testbed now correctly Locate()s a field ending with ' or #'
(or containing either of them if loPartialKey is specified. (I can't test in XE4/5
because my XE4 now says it's unlicenced since I upgraded to Win10 last week, thanks EMBA!)
I hestitate to call this a solution or even a work-around as yet, but it is at least worth testing.
I'm not sure whether I'd call the original version of GetFilterExpr bugged, because I'm not sure
what use-case its treatment of values containing quotes was intended to handle.

Copy a file to clipboard in Delphi

I am trying to copy a file to the clipboard. All examples in Internet are the same. I am using one from, http://embarcadero.newsgroups.archived.at/public.delphi.nativeapi/200909/0909212186.html but it does not work.
I use Rad Studio XE and I pass the complete path. In mode debug, I get some warnings like:
Debug Output:
Invalid address specified to RtlSizeHeap( 006E0000, 007196D8 )
Invalid address specified to RtlSizeHeap( 006E0000, 007196D8 )
I am not sure is my environment is related: Windows 8.1 64 bits, Rad Studio XE.
When I try to paste the clipboard, nothing happens. Also, seeing the clipboard with a monitor tool, this tool shows me error.
The code is:
procedure TfrmDoc2.CopyFilesToClipboard(FileList: string);
var
DropFiles: PDropFiles;
hGlobal: THandle;
iLen: Integer;
begin
iLen := Length(FileList) + 2;
FileList := FileList + #0#0;
hGlobal := GlobalAlloc(GMEM_SHARE or GMEM_MOVEABLE or GMEM_ZEROINIT,
SizeOf(TDropFiles) + iLen);
if (hGlobal = 0) then raise Exception.Create('Could not allocate memory.');
begin
DropFiles := GlobalLock(hGlobal);
DropFiles^.pFiles := SizeOf(TDropFiles);
Move(FileList[1], (PChar(DropFiles) + SizeOf(TDropFiles))^, iLen);
GlobalUnlock(hGlobal);
Clipboard.SetAsHandle(CF_HDROP, hGlobal);
end;
end;
UPDATE:
I am sorry, I feel stupid. I used the code that did not work, the original question that somebody asked, in my project, while I used the Remy's code, the correct solution, here in Stackoverflow. I thought that I used the Remy's code in my project. So, now, using the Remy's code, everything works great. Sorry for the mistake.
The forum post you link to contains the code in your question and asks why it doesn't work. Not surprisingly the code doesn't work for you any more than it did for the asker.
The answer that Remy gives is that there is a mismatch between ANSI and Unicode. The code is for ANSI but the compiler is Unicode.
So click on Remy's reply and do what it says: http://embarcadero.newsgroups.archived.at/public.delphi.nativeapi/200909/0909212187.html
Essentially you need to adapt the code to account for characters being 2 bytes wide in Unicode Delphi, but I see no real purpose repeating Remy's code here.
However, I'd say that you can do better than this code. The problem with this code is that it mixes every aspect all into one big function that does it all. What's more, the function is a method of a form in your GUI which is really the wrong place for it. There are aspects of the code that you might be able to re-use, but not factored like that.
I'd start with a function that puts an known block of memory into the clipboard.
procedure ClipboardError;
begin
raise Exception.Create('Could not complete clipboard operation.');
// substitute something more specific that Exception in your code
end;
procedure CheckClipboardHandle(Handle: HGLOBAL);
begin
if Handle=0 then begin
ClipboardError;
end;
end;
procedure CheckClipboardPtr(Ptr: Pointer);
begin
if not Assigned(Ptr) then begin
ClipboardError;
end;
end;
procedure PutInClipboard(ClipboardFormat: UINT; Buffer: Pointer; Count: Integer);
var
Handle: HGLOBAL;
Ptr: Pointer;
begin
Clipboard.Open;
Try
Handle := GlobalAlloc(GMEM_MOVEABLE, Count);
Try
CheckClipboardHandle(Handle);
Ptr := GlobalLock(Handle);
CheckClipboardPtr(Ptr);
Move(Buffer^, Ptr^, Count);
GlobalUnlock(Handle);
Clipboard.SetAsHandle(ClipboardFormat, Handle);
Except
GlobalFree(Handle);
raise;
End;
Finally
Clipboard.Close;
End;
end;
We're also going to need to be able to make double-null terminated lists of strings. Like this:
function DoubleNullTerminatedString(const Values: array of string): string;
var
Value: string;
begin
Result := '';
for Value in Values do
Result := Result + Value + #0;
Result := Result + #0;
end;
Perhaps you might add an overload that accepted a TStrings instance.
Now that we have all this we can concentrate on making the structure needed for the CF_HDROP format.
procedure CopyFileNamesToClipboard(const FileNames: array of string);
var
Size: Integer;
FileList: string;
DropFiles: PDropFiles;
begin
FileList := DoubleNullTerminatedString(FileNames);
Size := SizeOf(TDropFiles) + ByteLength(FileList);
DropFiles := AllocMem(Size);
try
DropFiles.pFiles := SizeOf(TDropFiles);
DropFiles.fWide := True;
Move(Pointer(FileList)^, (PByte(DropFiles) + SizeOf(TDropFiles))^,
ByteLength(FileList));
PutInClipboard(CF_HDROP, DropFiles, Size);
finally
FreeMem(DropFiles);
end;
end;
Since you use Delphi XE, strings are Unicode, but you are not taking the size of character into count when you allocate and move memory.
Change the line allocating memory to
hGlobal := GlobalAlloc(GMEM_SHARE or GMEM_MOVEABLE or GMEM_ZEROINIT,
SizeOf(TDropFiles) + iLen * SizeOf(Char));
and the line copying memory, to
Move(FileList[1], (PByte(DropFiles) + SizeOf(TDropFiles))^, iLen * SizeOf(Char));
Note the inclusion of *SizeOf(Char) in both lines and change of PChar to PByte on second line.
Then, also set the fWide member of DropFiles to True
DropFiles^.fWide := True;
All of these changes are already in the code from Remy, referred to by David.

IdHTTP.Put Error: HTTP/1.1405 Method Not Allowed

In Delphi XE2, I am trying to upload the lines of a memo to a file on my webspace with IdHTTP.Put:
procedure TForm1.btnUploadClick(Sender: TObject);
var
StringToUpload: TStringStream;
begin
StringToUpload := TStringStream.Create('');
try
StringToUpload.WriteString(memo.Lines.Text);
// Error: HTTP/1.1 405 Method Not Allowed.
IdHTTP1.Put(edtOnlineFile.Text, StringToUpload);
finally
StringToUpload.Free;
end;
end;
But I always get this error message:
So what must I do to avoid the error and make the upload?
It means the HTTP server does not support the PUT method on that URL (if at all). There is nothing you can do about that. You will likely have to upload your data another way, usually involving POST instead, or a completely different protocol, like FTP.
BTW, when using TStringStream like this, don't forget to reset the Position if you use the WriteString() method:
StringToUpload.WriteString(memo.Lines.Text);
StringToUpload.Position := 0;
Otherwise, use the constructor instead:
StringToUpload := TStringStream.Create(memo.Lines.Text);
Thanks for the above code, here is perhaps a little more information with a little helper function to assist with that Stream constructor which I found works for any string you pass through, even it contains binary stuff.
//Helper function to make JSON string correct for processing with POST / GET
function StringToStream(const AString: string): TStream;
begin
Result := TStringStream.Create(AString);
end;
//somewhere in your code, I am posting to Spring REST, encoding must be utf-8
IdHTTP1.Request.ContentType := 'application/json'; //very important
IdHTTP1.Request.ContentEncoding := 'utf-8'; //which encoding?
response := IdHTTP1.Put(URL, StringToStream(body)); //response,URL,body are declared as String

Reading value without using sections

How do I read value from INI file without using sections?
So instead of normal file:
[section]
name=value
it would result in this:
name=value
I wouldn't call it an INI file, then. Anyhow, for this the TStringList class fits perfectly.
Consider the file animals.txt:
dog=Sally
rat=Fiona
cat=Linus
And consider this code:
procedure TForm1.Button1Click(Sender: TObject);
begin
with TStringList.Create do
try
LoadFromFile('C:\Users\Andreas Rejbrand\Desktop\animals.txt');
ShowMessage(Values['dog']);
finally
Free;
end;
end;
There's a nice tutorial over here. For example, if iniFile is an instance of TIniFile, you can call the iniFile.ReadString method with an empty section specifier.
This is a late answer but here is some code I wrote for my project:
function GetPropertyValue(aFile, Key: string): string;
var
properties: TStringList;
begin
properties := TStringList.Create;
try
properties.LoadFromFile(aFile);
Result := properties.Values[key];
finally
properties.free;
end;
end;
procedure SetPropertyValue(aFile, Key, Value: string);
var
I: Integer;
properties: TStringList;
found: Boolean;
begin
found := False;
properties := TStringList.Create;
try
properties.LoadFromFile(aFile);
for I := 0 to properties.Count -1 do
begin
if properties.Names[I] = Key then
begin
properties[I] := Key + '=' + Value;
found := True;
Break
end;
end;
if not found then
begin
properties.Add(Key + '=' + Value);
end;
finally
properties.SaveToFile(aFile);
properties.free;
end;
end;
I think the question really needs more information. Often people will ask questions relating to what they think they need to do instead of asking questions related to what they are actually trying to accomplish.
Why do you need to do this instead of using the normal methods of reading the ini entries?
If these are existing ini files, then you should use the Tinifile.ReadSections to read the section names into a stringlist and then iterate through that list using Tinifile.ReadSectionValues to read all the section name/values pairs.
Are you reading existing INI files, or reading and writing your own files?
If these are your own files, then Andreas has a good answer above.

AdoQuery not working with SHOW: command

and I am tearing my hair out!!
Even something simple like this work:
procedure MyAdoQueryTest();
const MYSQL_CONNECT_STRING='Driver={MySQL ODBC 5.1 Driver};Server=%s;Port=3306;Database=%s;User=%s;Password=%s;Option=3;';
var AdoConnection : TADOConnection;
ADOQuery : TADOQuery;
Param : TParameter;
begin
AdoConnection := TADOConnection.Create(Nil);
AdoConnection.ConnectionString := Format(MYSQL_CONNECT_STRING,['localhost',
'mysql',
'root',
'']);
AdoConnection.LoginPrompt := False;
AdoConnection.Connected := True;
ADOQuery := TADOQuery.Create(Nil);
ADOQuery.Connection := AdoConnection;
ADOQuery.Sql.Clear();
ADOQuery.SQl.Add('SHOW :what_to_show');
Param := ADOQuery.Parameters.ParamByName('what_to_show');
Param.DataType := ftString;
Param.Value := 'databases';
ADOQuery.Prepared := true;
ADOQuery.Active := True;
end;
(btw, do I really need to use the 'Param' variable and 3 statements, or can I just ` ADOQuery.Parameters.ParamByName('what_to_show').Value := 'databases';?)
Anyway, when I run it, I get an exception at ADOQuery.SQl.Add('SHOW :what_to_show'); which says "Arguments are of the wrong type, are out of the acceptable range or are in conflict with one another".
What I am trying to do is to make 2 central functions: one which will accept and execute any SQL statement which will not return any data (such as INSERT INTO) and oen which will (such as SELECT).
I currently have these working with AdoConnection only, but am now trying to use AdoQuery because I want to parametrize my SQL statements to handle strings with quotes in them.
I can has halpz?
The error is here:
ADOQuery.SQl.Add('SHOW :what_to_show');
The :Param can only be used for values, not for dynamic column/keyword/table/database names.
This is because if it worked like that you'd have an SQL-injection risk depending on the contents of your parameter.
In order to fix that you'll have to inject your what_to_show thingy into the SQL-string.
Like so:
var
what_to_show: string;
begin
....
what_to_show:= 'tables';
ADOQuery.SQL.Text:= ('SHOW '+what_to_show);
....
Now it will work.
Warning
Make sure test everything you inject into the SQL to prevent users from being able inject their SQL-code into your queries.
Parameters prevent SQL injection, but because you cannot use them here you need to check them against a list of pre-approved values. e.g. a stringlist holding all the allowed what_to_shows.
Escaping or use of special chars is useless.
Safe injection example code
var
what_to_show: string;
i: integer;
inputapproved: boolean;
begin
....
what_to_show:= lower(trim(someinput));
i:= 0;
inputapproved:= false;
while (i < WhiteList.count) and not(inputapproved) do begin
inputapproved:= ( what_to_show = lower(Whitelist[i]) );
Inc(i);
end; {while}
if inputapproved then ADOQuery.SQL.Text:= ('SHOW '+what_to_show);
....

Resources