How to open MSI table in Delphi? - windows

I need to open some tables from MSI database, read this and place some rows in this with using Delphi (in my example is Delphi 7 but allowed other versions if it needed).
For example it would look like ORCA. Msi must be open, written to the table where it's can be edited and written to the msi file.
By default, Delphi can't open MSI tables as I thing but I found a JEDI Windows API where exists libraris like JwaMsi and JwaMsiQuery. But I can't find documentations or examples of using functions like
function MsiOpenProduct(szProduct: LPCTSTR; var hProduct: MSIHANDLE): UINT; stdcall;
{$EXTERNALSYM MsiOpenProduct}
By the way, while I search information about this I found this code:
const msilib = 'msi.dll';
type
MSIHANDLE = DWORD;
TMsiHandle = MSIHANDLE;
function MsiCloseHandle(hAny: MSIHANDLE):UINT;stdcall;external msilib name 'MsiCloseHandle';
function MsiOpenProduct(szProduct:LPCSTR;var hProduct:MSIHANDLE):UINT;stdcall;external msilib name 'MsiOpenProductA';
function MsiGetProductProperty(hProduct:MSIHANDLE;szProperty:LPCSTR;lpValueBuf:LPSTR;pcchValueBuf:LPDWORD):UINT;stdcall; external msilib name 'MsiGetProductPropertyA';
function MsiSetInternalUI(dwUILevel:INSTALLUILEVEL;phWnd:LPHWND):INSTALLUILEVEL;stdcall; external msilib name 'MsiSetInternalUI';
function GetMSIProperty(aProductCode:string):string;
var
msi:TMSIHandle;
t:string;
function _getmsiproperty(_name:string):string;
var
txt:PChar;
sz:DWORD;
begin
sz:=MAX_PATH;
txt:=AllocMem(sz+1);
if MsiGetProductProperty(msi,PChar(_name),txt,#sz)=ERROR_MORE_DATA then
begin
ReAllocMem(txt,sz+1);
MsiGetProductProperty(msi,PChar(_name),txt,#sz);
end;
SetString(Result,txt,sz);
FreeMem(txt,sz+1);
end;
begin
MsiSetInternalUI(2,nil); // скрываем GUI/hide GUI
if MsiOpenProduct(PChar(aProductCode),msi)=ERROR_SUCCESS then
begin
t:=_getmsiproperty('ARPPRODUCTICON'); // главная иконка приложения/main program icon
if t='' then t:=_getmsiproperty('ProductIcon');
if t='' then t:=_getmsiproperty('CompleteSetupIcon');
if t='' then t:=_getmsiproperty('CustomSetupIcon');
if t='' then t:=_getmsiproperty('InfoIcon');
if t='' then t:=_getmsiproperty('InstallerIcon');
if t='' then t:=_getmsiproperty('RemoveIcon');
if t='' then t:=_getmsiproperty('RepairIcon');
Result:=t;
MsiCloseHandle(msi);
end;
end;
What is better to use and where I can see documentation and/or examples?
P.S. Sorry for my English

I would use the COM-based API to MSI.
See this thread and the MSI API documentation

So, solution exist and I do it! It's not so gracefully but it's working...
At first, read articles from MSDN (in my case is articles about work with MSI database).
At second, you must understand what you need to do. In my case it is:
Open msi database
Read table names
Read needed table and show it
If it needed - make changes and save it
Here I show how I do the three first steps.
Preparing
Download JEDI Windows AP (for easer work with msi) and add to project JwaMsi.pas, JwaMsiDefs.pas and JwaMsiQuery.pas (do not forget about USES list). Dependencies will be added automatically.
Next, place on form all needed components.
Let's code!
1 Open msi database and 2 Read table names
Define variables for handlers and for buffers
var msi_handler_DB, msi_handler_view, msi_handler_record:TMSIHandle;
txt:PChar;
sz:DWORD;
Now we need take a list of tables (more info here) and place it in ListBox, for example
begin
sz:=MAX_PATH; //initialise
txt:=AllocMem(sz+1); //variables
OpenDialog1.Execute; //select file
If MsiOpenDatabase(PChar(OpenDialog1.FileName), MSIDBOPEN_DIRECT, msi_handler_DB)=ERROR_SUCCESS //check if DB is open
then begin //start reading
Listbox1.Clear; //prepare listbox for filling
MsiDatabaseOpenView(msi_handler_DB, 'SELECT * FROM _Tables',msi_handler_view); //prepare query to _Table
MsiViewExecute(msi_handler_view, msi_handler_record); //execute...
While not MsiViewFetch(msi_handler_view, msi_handler_record)=ERROR_NO_MORE_ITEMS //and fetch it in cycle until end of table
do begin
MsiRecordGetString(msi_handler_record,1,txt,sz); //read string
ListBox1.Items.Add(txt); //and write to listbox
end; //end of fetch cycle
MsiCloseAllHandles; //close handles (we don't need they more)
end; //stop reading
end;
3 Read needed table and show it
It's easy!
begin
edit1.text:=Listbox1.Items.ValueFromIndex[ListBox1.ItemIndex];
If MsiOpenDatabase(PChar(OpenDialog1.FileName), MSIDBOPEN_DIRECT, msi_handler_DB)=ERROR_SUCCESS //open database again
then begin
MsiDatabaseExport(msi_handler_DB, pchar(ListBox1.Items.Strings[ListBox1.ItemIndex]), 'C:\Windows\Temp', Pchar(ListBox1.Items.Strings[ListBox1.ItemIndex]+'.idt')); //export table to .idt
MsiCloseAllHandles; //and close handler again
//...
//here must be placed code for
//parsing .idt as tabulation separated file
//with wordwrap and show it.
//for example - in StringGrid
//...
DeleteFile('C:\Windows\Temp\'+ ListBox1.Items.Strings[ListBox1.ItemIndex]+'.idt'); //do not forget delete temporary .idt file. save our planet! :)
end;
end;
4 If it needed - make changes and save it
Just repeat third step from the end to beginning: export StringGrid to file (about .idt and it's structure you can read here: one, two, three), import in to MSI with property MsiDatabaseImport (do not forget open database before and append it after, also remember about close handlers) and delete temporary .idt file.
Good luck!
P.S. Sorry for my English, part two :)

Related

Apex Link builder value - item not displayed on the target page

I LOAD P70_NODE_ID from Javascript.
APEX 20.2
then run the following:
apex_util.set_session_state('P70_NODE_ID_OUT', :P70_NODE_ID);
When I show the session I see P70_NODE_ID_OUT and P70_NODE_ID. They both have the correct values
link to another page using the link builder
under item and value:
item P80_STEP_ID value :P70_NODE_ID_OUT
display p80_STEP_ID
I never see a value.
NOTE: The target page is a modal dialog built with the form wizard.
I have tried using a where statement P80_STEP_ID = :P70_NODE_ID_OUT
Still I don't see a value.
Is the problem related to inserting a value from Javascript?
How do I work around this?
Thanks
It turns out that when using redirect to another page in the same application, it uses the values that exist when the page is built and never looks at the values again. As a result, when values change after a page is loaded, the new values aren't used. The following allows you to use changed values when redirecting.
Used advice from the Apex community. This is what worked for me.
This uses apex_page.get_url and the technique you suggested. Tested good.
Javascript that runs when a node is selected in a flowchart:
$s('P72_NODE_ID', tagNum);
$s('P72_NODE_TYPE', selNode.getShape().getId());
$.event.trigger('submitNodeSel');
DynamicEvent
WHEN
event: custom
custom event: submitNodeSel
selectionType: javascriptexpression
javascript expression: document
True Action
Action: set value
set type: PL/SQL Function Body
DECLARE
STEP_DETAIL_URL varchar2(2000);
l_app number := v('APP_ID');
l_session number := v('APP_SESSION');
BEGIN
STEP_DETAIL_URL := apex_page.get_url(
p_page => '80'
, p_items => 'p80_step_id,p80_node_type'
, p_values => :p72_node_id || ',' || :p72_node_type
, p_plain_url => true);
RETURN STEP_DETAIL_URL;
END;
input: p72_node_id,p72_node_type
affected Elements
selection type: item(s)
item(s): P72_STEP_DETAIL_URL
BUTTON
Dynamic Action
execute javascript code
True Action
Javascript
makeDialogUrl( $v("P72_STEP_DETAIL_URL"));
Javascript Code
function makeDialogUrl (url) {
var i;
// replace unicode escapes
url = url.replace(/\\u(\d\d\d\d)/g, function(m,d) {
return String.fromCharCode(parseInt(d, 16));
});
apex.navigation.dialog(url,
{title:'Dialog In
Out',height:'auto',width:'720',maxWidth:'960',modal:true,dialog:null},
"t-Dialog-page--standard", "#STEP_DETAILS");
}
2 should be
under item and value: item P80_STEP_ID value &P70_NODE_ID_OUT.
Note leading ampersand & and trailing dot .
BTW, Apex would have done it for you if you picked columns from the LoV.

Oracle 11g R2 - dbms_xmldom: need to repeatedly append DOMDocumentFragment copies to different locations in a DOMDocument

I populate a DOMDocumentFragment, with the aim of copying its contents to a number of locations in a target DOMDocument.
I have tried pretty much everything I can think of to achieve this but it's a case of epic fail. The following simplified working code illustrates one method I have tried that I would expect to work:
declare
vTargetDoc dbms_xmldom.DOMDocument;
vFragDoc dbms_xmldom.DOMDocument;
vFrag dbms_xmldom.DOMDocumentFragment;
vAttachPointNodes dbms_xmldom.DOMNodeList;
vElt dbms_xmldom.DOMElement;
vTmpN dbms_xmldom.DOMNode;
begin
-- create the target document
vTargetDoc := dbms_xmldom.newDOMDocument(xmltype('<TargetDoc><AttachPoint></AttachPoint><AttachPoint></AttachPoint></TargetDoc>'));
-- create the source document to contain the fragment to be attached repeatedly
vFragDoc := dbms_xmldom.newDOMDocument();
-- create the fragment
vFrag := dbms_xmldom.createDocumentFragment(vFragDoc);
-- append element "A" to the fragment
vElt := dbms_xmldom.createElement(vFragDoc,'A');
vTmpN := dbms_xmldom.appendChild(dbms_xmldom.makeNode(vFrag),dbms_xmldom.makeNode(vElt));
-- identify all the attach points in the target document
vAttachPointNodes := dbms_xslprocessor.selectNodes(dbms_xmldom.makeNode(dbms_xmldom.getDocumentElement(vTargetDoc))
,'/TargetDoc/AttachPoint'
);
-- iterate through the attachpoints
for i in 0 .. dbms_xmldom.getLength(vAttachPointNodes) - 1 loop
-- import and attach the fragment to the current attachpoint
vTmpN := dbms_xmldom.appendChild(dbms_xmldom.item(vAttachPointNodes,i)
,dbms_xmldom.importNode(vTargetDoc
,dbms_xmldom.makeNode(vFrag)
,true
)
);
end loop;
-- print out the resultant target document XML
dbms_output.put_line(dbms_xmldom.getxmltype(vTargetDoc).getclobval());
end;
The noteworthy items in the code above are:
I create the fragment in a different document
On each attach point I import the fragment as a node (with deep = true)
The aim in this approach is to use importNode to copy the contents of the fragment from the source document as many times as I need to attach it in the target document.
The good news is that it does successfully copy the contents of the imported fragment to each required attach point.
The bad news is that it also appends a copy of the imported fragment at the end of the document as shown in the following illustrative output:
<TargetDoc>
<AttachPoint>
<A/> EXPECTED
</AttachPoint>
<AttachPoint>
<A/> EXPECTED
</AttachPoint>
</TargetDoc>
<A/> UNEXPECTED
<A/> UNEXPECTED
(the 2 FragmentContents duplicates appended on the end of the document are unexpected)
I can't figure out why it is creating the duplicates using this method, and neither can I find any other method that works.
Any help would be greatly appreciated. Thanks!
The problem seems to be the repeated calls to dbms_xmldom.makeNode(vFrag); you're doing that again each time round the loop, which creates another new node, and as at that point you aren't calling appendChild() it seems to stick it somewhere that looks a bit random.
You can refer to the result of your first call, vTmpN, instead:
for i in 0 .. dbms_xmldom.getLength(vAttachPointNodes) - 1 loop
-- import and attach the fragment to the current attachpoint
vTmpN := dbms_xmldom.appendChild(dbms_xmldom.item(vAttachPointNodes,i)
,dbms_xmldom.importNode(vTargetDoc
--,dbms_xmldom.makeNode(vFrag)
,vTmpN
,true
)
);
end loop;
which produces:
<TargetDoc>
<AttachPoint>
<A/>
</AttachPoint>
<AttachPoint>
<A/>
</AttachPoint>
</TargetDoc>
In this example it doesn't seem to matter that you're reassigning vTmpN - the next time round the loop that's still holding the node you want. You may prefer to have a separate variable to be safe (or clearer) though.

Oracle PL/SQL CASE UPDATE statement not working when called from ColdFusion

This is a strange one. I have a simple toggle update statement that when ran in TOAD works just fine (the test record's ID is 10000244999201)...
update myTable set
hasbeenread = case when (hasbeenread = 0) then 1 else 0 end
where id = 10000244999201;
This update statement is also in a package/procedure that can be called from ColdFusion and when it's called this way it does not work. The procedure runs and I get success back but the update doesn't toggle the hasbeenread column.
procedure toggleRead(p_webSession number, p_data lib.jsonclob, result out lib.jsonclob) is
ws websession%rowtype := lib.getWebSession(p_webSession);
pin lib.paramArray := lib.jsonToArray(p_data);
begin
--(1) Do perm checks here
--(2) Do data validation here
--(3) Code your process here
--(4) Send back a proper JSON/XML response.
delete from debuglog where title = 'gtest';
insert into debuglog ( username, seqno, title, cfd01, insertdate )
values ( 'GADMIN', 555, 'gtest', pin('id'), sysdate );
commit;
update myTable set
hasbeenread = case when (hasbeenread = 0) then 1 else 0 end
where id = pin('id');
result := lib.response(true,null,null);
exception
when others then
err.logAndEmailError( ws.fullName, gbody_version, 'toggleRead - p_data: '||p_data||', SQLERRM: '||sqlerrm );
end toggleRead;
I have verified the pin('id') value by looking at the debug log that happens right before the update, it is the correct id. I have even replaced pin('id') and hardcoded 10000244999201 into the where clause, that still didn't work. I tried putting a commit; after the update (which I shouldn't have to do because ColdFusion will commit when it comes back since I'm not using a cftransaction tag around it), that still didn't work. But when I remove the CASE statement and hardocde it to hasbeenread = 0 or hasbeenread = 1 it then works. So the problems looks like it has to do with the CASE statement. But like I said, it works fine when I run that update in TOAD. What am I missing?
Thanks!
Oracle Database 11g Enterprise Edition Release 11.2.0.2.0 - 64bit Production
ColdFusion version: 11,0,0,289974
UPDATE: Here is the ColdFusion call...
<cfstoredproc procedure="lib.getJson" datasource="#session.sv.ds#" returncode="no">
<cfprocparam type="in" cfsqltype="cf_sql_decimal" variable="webSession_in" value="#session.sv.csid#">
<cfprocparam type="in" cfsqltype="cf_sql_clob" variable="data" value="#z.data#">
<!---
How to get back a clob
Here they say to use CF_SQL_LONGVARCHAR
http://stackoverflow.com/questions/11053539/getting-clob-data-from-coldfusion-8
<cfprocparam type="out" cfsqltype="cf_sql_longvarchar" variable="result">
But instead we use CF_SQL_CLOB but then do the ArrayToList part you see below.
--->
<cfprocparam type="out" cfsqltype="cf_sql_clob" variable="z.result">
</cfstoredproc>
<cfif notnull(z.result)>
<cfset z.result = ArrayToList(z.result,"")>
</cfif>
<cfreturn z.result />
UPDATE: When I call the proc from TOAD it works, the update works and the column value toggles...
declare
r clob;
begin
c3.toggleread(146992,'{"ID":10000244999201}',r);
dbms_output.put_line(r);
end;
/
One common way it can fire twice is the use of a custom tag. For example if you put your stored proc in a custom tag and called it like this:
<Cf_toggleuser z="#mydata#"/>
Custom tags have 2 "execution modes" - start and end. They are referenced as attributes of the "thistag" scope within the contents of the tag. The tag (by design) runs two times - a start and and end. This is so you can run them with content inbetween - commonly done for altering layout or display content as in:
<cf_layout>
...some content here
</cf_layout>
But it also means if you intend to ONLY run the contents of the tag one time you need to include a check and only run it for one of the modes as in:
<cfif thistag.executionmode IS 'Start'>
...run the procedure
</cfif>
This may not be your problem but it is one nuance that occurs that is easy to miss. In your case the results were an error in data.
NOTE: you can also pull out the final slash in your tag (or the end tag) to cause it to execute only once. To illustrate:
This call only executes one time:
<Cf_toggleuser z="#mydata#">
While this call executes twice:
<Cf_toggleuser z="#mydata#"/>

load data from database on date selection

I am trying to load database data by using dbplannercalender1.
procedure TForm1.DBPlannerCalendar1DaySelect(Sender: TObject;
SelDate: TDateTime);
begin
with absQuery2 do
begin
absQuery2.Close;
absQuery2.sql.Clear;
ABSQuery2.SQL.Text:='select * from log where date = :a1';
ABSQUERY2.PARAMS.ParamByName('a1').value:= DBPlannerCalendar1.Date;
ABSQuery2.ExecSQL;
end;
end;
I get the error "date string expected"YYYY-MM-DD",but "="found at line 1..."
What am I doing wrong ?
Use trunced value
adoQuery.Parameters.paramByName('DataDal').value := trunc(edDate.Date);
Use AsDateTime instead of Value, to let the driver convert it to the proper format:
AbsQuery2.Params.ParamByName('a1').AsDateTime := DBPlannerCalendar1.Date;
If Absolute doens't support AsDateTime for some reason, format the date yourself:
AbsQuery2.Params.ParamByName('a1').Value :=
FormatDateTime('yyyy-mm-dd`, DBPlannerCalendar1.Date);
The first method is the best choice, because it works more portably between database engines (it works for all of them, because the driverr does the formatting into the proper arrangement for dates). Using the second means that if the DB expects something different than YYYY-MM-DD, you have to change the code everywhere that uses dates.

searching a text file the writing result to memo, lazarus

this segment in my program first ads a customer to a textfile (declared in public variables) and saves it to a texfile. onbutton1click is the procudere to search the string thats in the editbox and return the relevant customer details to memo. the add customer works fine and adds to textfile, however when i search it returns nothing onto the memo, just the memo caption, memo1. any way i can resolve this? sorry im a newb to this.
procedure TForm2.btnsaveClick(Sender: TObject);
begin
cusfname:= edit1.text ;
cuslname:= edit2.text;
adress:= edit3.text;
phone:= edit4.text;
password:= edit5.Text;
AssignFile(F, 'Data.txt');
append(F);
WriteLn(F, cusfname);
WriteLn(F, cuslname);
WriteLn(F, adress);
WriteLn(F, phone);
WriteLn(F, password);
CloseFile(F);
end;
procedure TForm2.Button1Click(Sender: TObject);
var
SearchFile : Textfile;
found: boolean;
search: string;
begin
search := edit1.text;
Assignfile(SearchFile, 'data.txt');
Reset(SearchFile);
found:= false;
repeat
found:= search = phone
until eof(searchfile) or found;
if found then
memo1.append(phone);
memo1.append(cusfname);
memo1.append(adress);
if not found then
showmessage('member not found');
end;
wonder where are the read statements? In the write function you have Write() statements, but in the reading code no read() statements?
In your code you do not read() from file. In other similar question (probably your own): runerror(102) file not assigned? there is read(). But I think you should use readln(), or even better use TStringList class from Classes unit with its LoadFromFile() method and Lines property.

Resources