Delphi ODAC: Disecting JMS messages from Oracle AQ - delphi

I'm trying to evaluate ODAC for using Oracle AQ.
The request queue contains JMS objects like these (but without linebreaks and other whitespace):
SYS.AQ$_JMS_BYTES_MESSAGE(
SYS.AQ$_JMS_HEADER(
'null','null','null','null','null','null',
SYS.AQ$_JMS_USERPROPARRAY(
SYS.AQ$_JMS_USERPROPERTY('Key1',100,'Value1','null',27),
SYS.AQ$_JMS_USERPROPERTY('Key2',100,'Value2','null',27),
SYS.AQ$_JMS_USERPROPERTY('Key3',100,'Value3','null',27),
SYS.AQ$_JMS_USERPROPERTY('Key4',100,'Value4','null',27),
SYS.AQ$_JMS_USERPROPERTY('Key5',100,'Value5','null',27),
SYS.AQ$_JMS_USERPROPERTY('Key6',100,'Value6','null',27),
SYS.AQ$_JMS_USERPROPERTY('Key7',100,'Value7','null',27),
SYS.AQ$_JMS_USERPROPERTY('Key8',100,'Value8','null',27),
SYS.AQ$_JMS_USERPROPERTY('Key9',100,'Value9','null',27),
SYS.AQ$_JMS_USERPROPERTY('Key10',100,'Value10.0','null',27),
SYS.AQ$_JMS_USERPROPERTY('Key11',100,'Value11','null',27),
SYS.AQ$_JMS_USERPROPERTY('Key12',100,'Value12','null',27),
SYS.AQ$_JMS_USERPROPERTY('Key13',100,'Value13','null',27),
SYS.AQ$_JMS_USERPROPERTY('Key14',100,'Value14','null',27),
SYS.AQ$_JMS_USERPROPERTY('Key15',100,'Value15','null',27),
SYS.AQ$_JMS_USERPROPERTY('Key16',100,'Value16','null',27),
SYS.AQ$_JMS_USERPROPERTY('Key17',100,'Value17','null',27)
)
),
4168,'null','oracle.sql.BLOB#959acc'
)
I can receive the underlying object (a string Payload comes back as an empty string, but a TOraObject PayLoad contains data).
I'm trying to disscect the TOraObject PayLoad, and am looking for a table that converts the DataType values into the correct AttrXxxx[Name] property calls.
OraType.AttributeCount:4
OraType.Name:"SYS"."AQ$_JMS_BYTES_MESSAGE"
OraType.DataType:15
Attribute[0].Name:HEADER
Attribute[0].DataType:15
OraType.AttributeCount:7
OraType.Name:"SYS"."AQ$_JMS_HEADER"
OraType.DataType:15
Attribute[0].Name:REPLYTO
Attribute[0].DataType:15
OraType.AttributeCount:3
OraType.Name:"SYS"."AQ$_AGENT"
OraType.DataType:15
Attribute[0].Name:NAME
Attribute[0].DataType:1
Attribute[1].Name:ADDRESS
Attribute[1].DataType:1
Attribute[2].Name:PROTOCOL
Attribute[2].DataType:5
Attribute[1].Name:TYPE
Attribute[1].DataType:1
Attribute[2].Name:USERID
Attribute[2].DataType:1
Attribute[3].Name:APPID
Attribute[3].DataType:1
Attribute[4].Name:GROUPID
Attribute[4].DataType:1
Attribute[5].Name:GROUPSEQ
Attribute[5].DataType:5
Attribute[6].Name:PROPERTIES
Attribute[6].DataType:17
OraType.AttributeCount:1
OraType.Name:"SYS"."AQ$_JMS_USERPROPARRAY"
OraType.DataType:17
Attribute[0].Name:ELEMENT
Attribute[0].DataType:15
OraType.AttributeCount:5
OraType.Name:"SYS"."AQ$_JMS_USERPROPERTY"
OraType.DataType:15
Attribute[0].Name:NAME
Attribute[0].DataType:1
Attribute[1].Name:TYPE
Attribute[1].DataType:5
Attribute[2].Name:STR_VALUE
Attribute[2].DataType:1
Attribute[3].Name:NUM_VALUE
Attribute[3].DataType:5
Attribute[4].Name:JAVA_TYPE
Attribute[4].DataType:5
Attribute[1].Name:BYTES_LEN
Attribute[1].DataType:5
Attribute[2].Name:BYTES_RAW
Attribute[2].DataType:1
Attribute[3].Name:BYTES_LOB
Attribute[3].DataType:102
By trial and error, I have come so far:
case DataType of
102:
LOB := ObjectPayLoad.AttrAsLob[Name];
15:
AttributeOraObject := ObjectPayLoad.AttrAsObject[Name];
17:
AttributeOraArray := ObjectPayLoad.AttrAsArray[Name];
else
begin
PayLoadAttributeAsString := ObjectPayLoad. AttrAsString[Name];
Logger.Log(' "%s"', [PayLoadAttributeAsString]);
end;
end;
A more complete list is welcome :-)
After this, I will need to research the other way around: generating the right TOraObject that has a JMS content in it.
Tips for that are also welcome.
--jeroen

Edit:
ODAC has multiple units defining constants.
The constant dtOraBlob with value 102 is in the OraClasses unit; constants defining DataType values start with the prefix dt, regardless of the unit that defines them.
Original:
I have found a few of these constants in the MemData unit:
case DataType of
102:
LOB := OraObject.AttrAsLob[Name];
MemData.dtObject: // 15
begin
AttributeOraObject := OraObject.AttrAsObject[Name];
LogOraObject(AttributeOraObject, Level+1);
end;
MemData.dtArray: // 17
begin
AttributeOraArray := OraObject.AttrAsArray[Name];
LogOraArray(AttributeOraArray, Level);
end;
MemData.dtFloat: // 5
begin
AttributeFloat := OraObject.AttrAsFloat[Name];
Logger.Log(Prefix+'"%g"', [AttributeFloat]);
end;
MemData.dtString: // 1
begin
PayLoadAttributeAsString := OraObject.AttrAsString[Name];
Logger.Log(Prefix+'"%s"', [PayLoadAttributeAsString]);
end;
else
begin
PayLoadAttributeAsString := OraObject.AttrAsString[Name];
Logger.Log(Prefix+'"%s"', [PayLoadAttributeAsString]);
end;
end;
I can't find the 102 constant though, but I'm pretty sure it is for a LOB field.
Anyone who can confirm that?
--jeroen

Related

Receiving syntax error while using ADOQuery to insert into database

All names and data types were checked to ensure no incorrect info/fields were used.
A with Datamodule was stated at the top of the code, but there is too much to post from the rest, this is the only part which doesn't work:
sEncPass := objAlgorithms.CipherEn(sPass,13);
//start insert code
qryUserInfo.SQL.Clear;
qryUserInfo.SQL.Add('INSERT INTO LoginInfo(ID,Login,Password)');
qryUserInfo.SQL.Add('VALUES(:ID,:Login,:Password)');
with qryUserInfo.Parameters do
begin
ParamByName('ID').Value := sID;
ParamByName('Login').Value := sUsername;
ParamByName('Password').Value := sEncPass;
end;
qryUserInfo.ExecSQL;
qryUserInfo.SQL.Clear;
qryUserInfo.SQL.Add('INSERT INTO CustomerInfo(ID,Name,Surname,TelNo,Email)');
qryUserInfo.SQL.Add('VALUES (:ID,:Name,:Surname,:TelNo,:Email)');
with qryUserInfo.Parameters do
begin
ParamByName('ID').Value := sID;
ParamByName('Name').Value:= sName;
ParamByName('Surname').Value:= sSurname;
ParamByName('TelNo').Value:= sCell;
ParamByName('Email').Value:= sRAddres;
end;
qryUserInfo.ExecSQL;
The fields I am using in the DB (all are strings):
Thanks everyone who helped, found a fix by adding ` to the field names:
qryUserInfo.SQL.Add('INSERT INTO CustomerInfo(`ID`,`Name`,`Surname`,`TelNo`,`Email`)');

Is there any point of refactoring LUT array to case statement?

I've got the following LUT (lookup table) for retrieval of display name for pseudo-PChar (all these predefined PChars are integers under their skin, you know) input:
const
RT_MIN = DWORD(RT_CURSOR);
RT_MAX = DWORD(RT_MANIFEST);
ResourceTypes: array [RT_MIN..RT_MAX] of PChar = (
'Hardware-dependent cursor',
'Bitmap',
'Hardware-dependent icon',
'Menu',
'Dialog box',
'String-table entry',
'Font directory',
'Font',
'Accelerator table',
'Application-defined resource (raw data)',
'Message-table entry',
'Hardware-independent cursor',
nil, { unknown, reserved or not used }
'Hardware-independent icon',
nil, { unknown, reserved or not used }
'Version',
'Dialog Include',
nil, { unknown, reserved or not used }
'Plug and Play',
'VxD',
'Animated cursor',
'Animated icon',
'HTML resource',
'Side-by-Side Assembly Manifest'
);
Will I get any advantages/disadvantages in rewriting that as case statement? Are there any advantages/disadvantages in leaving that as is?
I think that using an array is the fastest method. If you e.g. query ResourceTypes[2], the program will first look at ResourceTypes[2], dereference the PChar and output the zero terminated string. If the compiler is smart, it could recognize that the strings are unchangeable and so it could place all strings directly in the array, so you would save one dereferencing operation. (For those who are interested in it, can view the memory contents using an hex-editor like HxD to check if this is true or not).
Another problem which might happen in future could be following scenario: Let's say Microsoft defines a new resource type which is something very special, and so it gets a large number like $FFFF . If you are using case of, you can simply add 2 lines of code to add this new resource type. By having a lookup-table (or LUT, this abbreviation is new to me), you would have a problem then, since you would need to create an array with size 65535 whose contents are to 99% just nils.
I would accomplish it by creating a function:
function GetHumanFriendlyResourceTypeName(AResourceType: PChar): string;
begin
if not Is_IntResource(AResourceType) then
begin
result := AResourceType;
end
else
begin
case Integer(AResourceType) of
Integer(RT_CURSOR):
result := 'Hardware-dependent cursor';
Integer(RT_BITMAP):
result := 'Bitmap';
Integer(RT_ICON):
result := 'Hardware-dependent icon';
Integer(RT_MENU):
result := 'Menu';
Integer(RT_DIALOG):
result := 'Dialog box';
Integer(RT_STRING):
result := 'String-table entry';
Integer(RT_FONTDIR):
result := 'Font directory';
Integer(RT_FONT):
result := 'Font';
Integer(RT_ACCELERATOR):
result := 'Accelerator table';
Integer(RT_RCDATA):
result := 'Application-defined resource (raw data)';
Integer(RT_MESSAGETABLE):
result := 'Message-table entry';
Integer(RT_GROUP_CURSOR):
result := 'Hardware-independent cursor';
Integer(RT_GROUP_ICON):
result := 'Hardware-independent icon';
Integer(RT_VERSION):
result := 'Version';
Integer(RT_DLGINCLUDE):
result := 'Dialog Include';
Integer(RT_PLUGPLAY):
result := 'Plug and Play';
Integer(RT_VXD):
result := 'VxD';
Integer(RT_ANICURSOR):
result := 'Animated cursor';
Integer(RT_ANIICON):
result := 'Animated icon';
Integer(RT_HTML):
result := 'HTML resource';
Integer(RT_MANIFEST):
result := 'Side-by-Side Assembly Manifest';
else
result := Format('(Unknown type %d)', [Integer(AResourceType)]);
end;
end;
end;
Here is a demonstration of the code:
procedure TForm1.Button1Click(Sender: TObject);
begin
// Hardware-dependent icon
ShowMessage(GetHumanFriendlyResourceTypeName(MAKEINTRESOURCE(3)));
// (Unknown type 123)
ShowMessage(GetHumanFriendlyResourceTypeName(MAKEINTRESOURCE(123)));
// AVI
ShowMessage(GetHumanFriendlyResourceTypeName(PChar('AVI')));
end;
The performance is not as high as in your solution, but this function has several advantages:
This function is much easier to read since every RT_ constant is standing in front of its human-friendly name. So the code is also much better to maintain. In the LUT, the human-friendly names could be accidently interchanged (also since no comment in front of each human-friendly name indicates the official RT_ constant name).
This function does also show a nice human-friendly string "(Unknown type 123)" if the identifier is unknown.
This function will also dereference the string if it is not a predefined type (RT_)
Using this function you can internationalize your application either statically by putting the strings into resourcestrings or dynamically by querying a translation function/stringlist.

Empty string in Delphi / Windows combo box causes access exception

I've got a Delphi 7.0 application that throws a memory access exception / message box every time it writeln's an empty string from the string list associated with a combo box:
csvstrlst := combobox1.Items;
csvstrlst.clear;
csvstrlst.add(''); //problem
csvstrlst.add('a'); //no problem
csvstrlst.add(''); //problem
csvstrlst.add('b'); //no problem
//throws memory access messages (I think the writeln writes a line though)
for n := 1 to csvstrlst.Count do begin
writeln(out_file,csvstrlst.strings[n-1])
end;
//throws memory access messages (writeln does write a comma text string though)
writeln(out_file,csvstrlst.commatext);
Running on Windows 7 or XP. As application or in D7 IDE. Combobox with empty string items also causes the same error if the parent of the form it is on is changed.
Has anyone else ever seen or heard of this problem? Any other information available at all?
This is a known and solved bug described in QC:
TCombobox gives AV when selecting empty item from dropdown
Although this is a bug, you should not reuse parts from controls to perform some data tasks as described in your question.
You will not save anything doing so, but getting most the time unwanted sideeffects (controls get repainted and/or fire events)
If you want to have a TStringList then create an instance.
csvstrlst := TStringList.Create;
try
// csvstrlst.Clear;
csvstrlst.Add( '' );
csvstrlst.Add( 'a' );
csvstrlst.Add( '' );
csvstrlst.Add( 'b' );
for n := 0 to csvstrlst.Count - 1 do
begin
WriteLn( out_file, csvstrlst[n] )
end;
WriteLn( out_file, csvstrlst.CommaText );
finally
csvstrlst.Free;
end;
As Sir Rufo has discovered the issue is a VCL bug introduced in Delphi 7 as described in QC#2246. According to that report the bug is resolved in a build with major version number 7 so you may be able to fix the problem by applying the latest Delphi 7 updates.
If not then you can fix the problem from the outside. I don't actually have a Delphi 7 installation to test this on, but I believe that this interposer class will work.
type
TFixedComboBoxStrings = class(TComboBoxStrings)
protected
function Get(Index: Integer): string; override;
end;
TComboBox = class(StdCtrls.TComboBox)
protected
function GetItemsClass: TCustomComboBoxStringsClass; override;
end;
function TFixedComboBoxStrings.Get(Index: Integer): string;
var
Len: Integer;
begin
Len := SendMessage(ComboBox.Handle, CB_GETLBTEXTLEN, Index, 0);
if (Len <> CB_ERR) and (Len > 0) then
begin
SetLength(Result, Len);
SendMessage(ComboBox.Handle, CB_GETLBTEXT, Index, Longint(PChar(Result)));
end
else
SetLength(Result, 0);
end;
function TComboBox.GetItemsClass: TCustomComboBoxStringsClass;
begin
Result := TFixedComboBoxStrings;
end;
The bug that was introduced in Delphi 7 is simply that the if statement reads:
if Len <> CB_ERR then
So, when Len is zero, that is when the item is the empty string, the True branch of the if is chosen. Then, the SendMessage becomes:
SendMessage(ComboBox.Handle, CB_GETLBTEXT, Index, Longint(PChar('')));
Now, PChar('') has special treatment and evaluates to a pointer to read only memory containing a zero character. And so when the combo box window procedure attempts to write to that memory, an access violation occurs because the memory is read only.

Improve speed of own debug visualizer for Delphi 2010

I wrote Delphi debug visualizer for TDataSet to display values of current row, source + screenshot: http://delphi.netcode.cz/text/tdataset-debug-visualizer.aspx . Working good, but very slow. I did some optimalization (how to get fieldnames) but still for only 20 fields takes 10 seconds to show - very bad.
Main problem seems to be slow IOTAThread90.Evaluate used by main code shown below, this procedure cost most of time, line with ** about 80% time. FExpression is name of TDataset in code.
procedure TDataSetViewerFrame.mFillData;
var
iCount: Integer;
I: Integer;
// sw: TStopwatch;
s: string;
begin
// sw := TStopwatch.StartNew;
iCount := StrToIntDef(Evaluate(FExpression+'.Fields.Count'), 0);
for I := 0 to iCount - 1 do
begin
s:= s + Format('%s.Fields[%d].FieldName+'',''+', [FExpression, I]);
// FFields.Add(Evaluate(Format('%s.Fields[%d].FieldName', [FExpression, I])));
FValues.Add(Evaluate(Format('%s.Fields[%d].Value', [FExpression, I]))); //**
end;
if s<> '' then
Delete(s, length(s)-4, 5);
s := Evaluate(s);
s:= Copy(s, 2, Length(s) -2);
FFields.CommaText := s;
{ sw.Stop;
s := sw.Elapsed;
Application.MessageBox(Pchar(s), '');}
end;
Now I have no idea how to improve performance.
That Evaluate needs to do a surprising amount of work. The compiler needs to compile it, resolving symbols to memory addresses, while evaluating properties may cause functions to be called, which needs the debugger to copy the arguments across into the debugee, set up a stack frame, invoke the function to be called, collect the results - and this involves pausing and resuming the debugee.
I can only suggest trying to pack more work into the Evaluate call. I'm not 100% sure how the interaction between the debugger and the evaluator (which is part of the compiler) works for these visualizers, but batching up as much work as possible may help. Try building up a more complicated expression before calling Evaluate after the loop. You may need to use some escaping or delimiting convention to unpack the results. For example, imagine what an expression that built the list of field values and returned them as a comma separated string would look like - but you would need to escape commas in the values themselves.
Because Delphi is a different process than your debugged exe, you cannot direct use the memory pointers of your exe, so you need to use ".Evaluate" for everything.
You can use 2 different approaches:
Add special debug dump function into executable, which does all value retrieving in one call
Inject special dll into exe with does the same as 1 (more hacking etc)
I got option 1 working, 2 should also be possible but a little bit more complicated and "ugly" because of hacking tactics...
With code below (just add to dpr) you can use:
Result := 'Dump=' + Evaluate('TObjectDumper.SpecialDump(' + FExpression + ')');
Demo code of option 1, change it for your TDataset (maybe make CSV string of all values?):
unit Unit1;
interface
type
TObjectDumper = class
public
class function SpecialDump(aObj: TObject): string;
end;
implementation
class function TObjectDumper.SpecialDump(aObj: TObject): string;
begin
Result := '';
if aObj <> nil then
Result := 'Special dump: ' + aObj.Classname;
end;
initialization
//dummy call, just to ensure it is linked c.q. used by compiler
TObjectDumper.SpecialDump(nil);
end.
Edit: in case someone is interested: I got option 2 working too (bpl injection)
I have not had a chance to play with the debug visualizers yet, so I do not know if this work, but have you tried using Evaluate() to convert FExpression into its actual memory address? If you can do that, then type-cast that memory address to a TDataSet pointer and use its properties normally without going through additional Evaluate() calls. For example:
procedure TDataSetViewerFrame.mFillData;
var
DS: TDataSet;
I: Integer;
// sw: TStopwatch;
begin
// sw := TStopwatch.StartNew;
DS := TDataSet(StrToInt(Evaluate(FExpression)); // this line may need tweaking
for I := 0 to DS.Fields.Count - 1 do
begin
with DS.Fields[I] do begin
FFields.Add(FieldName);
FValues.Add(VarToStr(Value));
end;
end;
{
sw.Stop;
s := sw.Elapsed;
Application.MessageBox(Pchar(s), '');
}
end;

Delphi 5 & Crystal XI Rel. 2 (RDC) how to?

I'm trying to work with the class from JosephStyons but I do get an "Invalid Index" Error on the line where the "User ID" should get set.
FRpt.Database.Tables[i].ConnectionProperties.Item['User ID'] := edUserName.Text;
Here's my environment:
WinXP Sp3, Crystal Reports Developer XI Rel.2 SP4, Delphi 5 Update Pack 1
Any help or ideas greatly appreciated!
Thx,
Reinhard
Your value for [i] could be the culprit...I can't remember for sure but I believe the first table will be Table[1] instead of Table[0] as one would expect.
I altered my loop to use:
CrTables := CrDatabase.Tables;
for crTableObj in crTables do
You might try stepping through the table using a for loop as shown above or by starting with 1 instead of 0.
I hope this helps.
Put a break point on that line and use Evaluate/Modify.
It will return an error if you try something invalid.
Examine FRpt.Database.Tables[i] and see if it's valid for what you think are the min and max values for i.
If Tables is an array, one way to avoid that is to use ...Low(Tables) to High(Tables)
If you get your Table Ok, examine FRpt.Database.Tables[i].ConnectionProperties.Item['User ID'] and see if it's valid.
It might be that the Item getter does not like the space embedded in "User ID". Some products need either to surround by special characters like "[User ID]", other to replace by an underscore like "User_ID"
Are you also setting the password, server name and database name?
procedure TReports.LogonToDBTables(cReport:
CrystalDecisions.CrystalReports.Engine.ReportDocument;
ConnInfo: ConnectionInfo);
var
CrDataBase: Database;
CrTables: Tables;
CrTableObj: TObject;
CrTable: Table;
CrTableLogonInfo: TableLogonInfo;
iSubReportIndex: smallint;
begin
CrDataBase := CReport.Database;
CrTables := CrDatabase.Tables;
cReport.DataSourceConnections[0].IntegratedSecurity := False;
for crTableObj in crTables do
begin
crTable := CrystalDecisions.CrystalReports.Engine.Table(crTableObj);
crTableLogonInfo := crTable.LogOnInfo;
crTableLogonInfo.ConnectionInfo := ConnInfo;
crTable.ApplyLogOnInfo(crTableLogonInfo);
end;
end;
function TReports.GetConnectionInfo(): ConnectionInfo;
var
cTemp: ConnectionInfo;
begin
cTemp := ConnectionInfo.Create();
cTemp.AllowCustomConnection := True;
cTemp.ServerName := GetServerName();
cTemp.DatabaseName := GetDBName();
cTemp.UserID := GetDBUserID();
cTemp.Password := GetDBPassword();
Result := cTemp;
end;

Resources