This section explains how to manipulate RDF with a script.
Templates can be used to extract data from an RDF datasource and build content based on the data. However, the datasources can be examined from a script. This also allows you to modify the datasource. This would be necessary to, for example, add a new bookmark. From an element built from a template, you can grab the datasource from it and then pick out individual resources from it.
The XPCOM interface to RDF involves a number of interfaces. The following lists some of the interfaces invloved:
| nsIRDFService | A global RDF service. It is used to generate resource objects that can uniquely identify a resource with-in an RDF data source. |
| nsIRDFDataSource | An RDF datasource, either a built-in one or one from an RDF file. Methods allow you to get and set values. |
| nsIRDFContainer | A container node with-in an RDF data source. Methods allow you to add and remove resources. |
| nsIRDFContainerUtils | This interface has some handy container methods to create Seq, Bag and Alt resources. |
In the find files dialog, we could implement the ability to store the most recently searched for items. The search text field could be replaced by an editable drop-down that contains a list of items that were recently searched for. We will add this capability now.
This will really only work if the dialog has access to a location on disk where the recent search items list can be stored. The most likely places for this are the user's profile directory or a directory the user chooses themselves. Although we won't do that here, the user's profile directory can be retreived using the nsIFileLocator interface. To simplify the example, we'll just put a file path directly in the XUL in a datasources attribute.
We could store the recent searches list in a plain text file. However, we can use RDF which already has the ability to read and write its data and update a widget generated from a template automatically. First, the changes to the XUL file. We'll replace the text field with a drop-down list. Replace the value of the datasources attribute with a suitable path. (The file itself does not need to exist -- it will be created automatically when the data is saved).
<menulist id="find-text" flex="1" style="min-width: 15em;"
editable="true"
datasources="file:///mozilla/recents.rdf"
ref="urn:findfile:recent">
<template>
<menupopup>
<menuitem value="rdf:http://www.example.com/recent#Value" uri="rdf:*"/>
</menupopup>
</template>
</menulist>
|
All XUL elements that have their children generated by a template have a database property that refers to a nsIRDFDataSource object. This object can then be used to read from and modify the data source used. The database property is placed on the element that has the datasources attribute. This will typically be a tree or, as is the case here, a menulist element.
The database property contains a list (actually an nsISimpleEnumerator) of each of the datasources that were specified in the datasources attribute. That means that we need to interate over each element, even if there is only one. The following example shows how to do this, assuming only one datasource exists:
var dsource;
var menulist=document.getElementById("find-text");
var sources=menulist.database.GetDataSources();
if (sources.hasMoreElements()){
dsource=sources.getNext();
}
dsource=dsource.QueryInterface(Components.interfaces.nsIRDFDataSource);
|
First, we get a reference to a menulist, which here has an id of find-text. Next we get the list of datasources from the menulist. The nsISimpleEnumerator interface has two methods (it is similar to Java's Enumeration interface). We loop through the elements in the enumeration and, since we assume there is only one, we'll just get it with the getNext method. Finally, we'll call QueryInterface to ensure that it is an nsIRDFDataSource.
We'll use code similar to this to create the recent searches list. First, however, let's initialize the components that we want to use. We'll need three components. The interface nsIRDFService will be used to create resource objects. The interface nsIRDFContainer will be used to add resources to the data source. The third interface, nsIRDFContainerUtils will be used only when the recent searches list is first used, to create the root node. In a script file (findfile.js), add the following code to the top of it. This will be executed when the find files dialog is loaded.
var RDFC = 'component://netscape/rdf/container'; RDFC = Components.classes[RDFC].getService(); RDFC = RDFC.QueryInterface(Components.interfaces.nsIRDFContainer); var RDFCUtils = 'component://netscape/rdf/container-utils'; RDFCUtils = Components.classes[RDFCUtils].getService(); RDFCUtils = RDFCUtils.QueryInterface(Components.interfaces.nsIRDFContainerUtils); var RDF = 'component://netscape/rdf/rdf-service' RDF = Components.classes[RDF].getService(); RDF = RDF.QueryInterface(Components.interfaces.nsIRDFService); |
This code will create the three services that we need to use. The syntax is similar to other XPCOM object creation code. The first three lines get a reference to an nsIRDFContainer object. Next, we perform a similar operation to get the nsIRDFContainerUtils object. Finally, we repeat again for the nsIRDFService.
Next, we create an initialize function, which we'll call in the onload handler of the window. It will be executed when the window is displayed. With-in this code, we'll add code to initialize the RDF objects we created above.
findfile.xul:
<window onload="initSearchList()" ... >
findfile.js:
function initSearchList()
{
var recentlist=document.getElementById("find-text");
var sources=recentlist.database.GetDataSources();
var rootnode=RDF.GetResource("urn:findfile:recent");
while (sources.hasMoreElements()){
try {
dsource=sources.getNext();
dsource=dsource.QueryInterface(Components.interfaces.nsIRDFDataSource);
RDFC.Init(dsource,rootnode);
return;
} catch (e){}
}
RDFCUtils.MakeSeq(dsource,rootnode);
RDFC.Init(dsource,rootnode);
}
|
Let's break down the initSearchList function:
var recentlist=document.getElementById("find-text");
var sources=recentlist.database.GetDataSources();
First, we get a reference to the menulist
element that has the datasource on it. It has a
database property that holds the datasources
that are present. We get a reference to the available datasources
and assign it to the variable sources.var rootnode=RDF.GetResource("urn:findfile:recent");
A resource object is generated with the given URI. This will be the
root resource and will be an RDF Seq element
that holds a list of resources, one for each item in the recent searches
list. This function does not get anything from the datasource, it only
converts the URI into a resource identifier. Instead of hard-coding the
URI, we could also get it from the ref
attribute.while (sources.hasMoreElements()){
try {
dsource=sources.getNext();
dsource=dsource.QueryInterface(Components.interfaces.nsIRDFDataSource);
Next, we loop over each datasource to get the right one.RDFC.Init(dsource,rootnode);This function initializes the RDF Container (the nsIRDFContainer interface) with the datasource and the root node. Later, we can use the container object to add new resources inside the container. We'll need to do this to add a searched item to the datasource. An error will occur if the datasource or root node does not exist (for example, if the RDF file was not found). The code was put into a try-catch block to catch the error.
return;If no exception occured, we can just return. Otherwise, we'll need to create the root node ourselves.
RDFCUtils.MakeSeq(dsource,rootnode); RDFC.Init(dsource,rootnode);If the return was never reached, it means an error occured, most likely because the root node did not exist. In this case, we call the method MakeSeq of the nsIRDFContainerUtils interface in order to create it. Similar functions exist for creating bags and alts. (MakeBag and MakeAlt).
The interface nsIRDFService contains a method GetResource that creates a resource object for us, from the string passed in as an argument. This method does not get the value of anything, it simply converts a string into a resource object that can be used to get the value from the datasource. The RDF interfaces do not use strings but instead use resources to refer to things. The value returned by GetResource is of the type nsIRDFResource.
Now that the objects have been initialized, we can add and remove resources from it. There are two methods needed depending on whether you want to add a resource to a container or add a resource to another resource (called an assertion.) These two cases correspond to adding a bookmark and adding a property such as the title or URL to a bookmark.
We'll add a new entry to the searched for items list when the user clicks the Find button. We'll oversimplify it a bit in several ways. For one, we won't bother checking for duplicate entries. Second, we won't concern ourselves about limiting the length of the list.
Let's add another function that is called from with-in the doFind function.
function doFind()
{
var recentlist=document.getElementById("find-text");
var fldval=document.getAnonymousNodes(recentlist)[1].value;
addSearchedItem(fldval);
.
.
.
|
This code gets the value of the menu list's text. We would normally just use 'recentlist.value' to do this, but this doesn't seem to be implemented yet. Hence, we need to use a workaround. The section on XBL describes what the syntax actually means. We pass the text to the function addSearchedItem which will be defined next.
function addSearchedItem(txt)
{
var newnode=RDF.GetResource("urn:findfile:recent:item"+(RDFC.GetCount()+1));
var value=RDF.GetResource("http://www.example.com/recent#Value");
var newvalue=RDF.GetLiteral(txt);
dsource.Assert(newnode,value,newvalue,true);
RDFC.InsertElementAt(newnode,1,true);
dsource.QueryInterface(Components.interfaces.nsIRDFRemoteDataSource).Flush();
}
|
This code does three things, it adds a new resource, it adds a new assertion which holds the value, and then it writes out the modified datasource. Let's break down the code:
var newnode=RDF.GetResource("urn:findfile:recent:item"+(RDFC.GetCount()+1));
This line creates a resource object for the resource we'll be adding.
The function GetCount returns a count of the number of resources that
exist in the container already. This allows us to generate a unique
URI. We could also call GetAnonymousResource (instead of GetResource)
which takes no parameters and generates a random unique URI.var value=RDF.GetResource("http://www.example.com/recent#Value");
We'll be setting the Value property of the resource to the recent
searched text. You could use any property name (and URL) as long as
it's consistent. You'll notice that it has the same value as the
value attribute of the
menuitem element in the XUL file.var newvalue=RDF.GetLiteral(txt);The GetLiteral function generates a RDF string object that will hold the searched for text, that was passed in through the txt argument. We don't use GetResource here because we are assigning a value to a resource.
dsource.Assert(newnode,value,newvalue,true);This line will add an assertion to the RDF datasource. In this case, it says that the 'Value' of the resource 'urn:findfile:recent:itemX' is the literal object that was created in the previous line, where X is the number returned from the GetCount function. However, this is only half of what needs to be done. We still need to say that the resource is one of the recent items.
RDFC.InsertElementAt(newnode,1,true);This line adds the resource to the container. Here, we insert it at position 1. (Not that the first element is 1 and not 0.) We could insert it anywhere, or call AppendElement instead to add it to the end. The menulist template will now detect the new resource, and will have an extra row in the list.
dsource.QueryInterface(Components.interfaces.nsIRDFRemoteDataSource).Flush();Finally, we write the datasource to disk using the Flush function. This function is not a part of the nsIRDFDataSource interface, so we have to call QueryInterface to convert the datasource into the right interface, nsIRDFRemoteDataSource, first.
| Not all datasources can be modified. All datasources loaded from file and resource URLs can be written to as well as some of the internal datasources. |
If you open the find files dialog now and enter some text, and press Find, you'll find the text appears as one of the choices in the text drop-down. Even if you exit and reload, the text will remain.
In order to check for duplicate entries, we could check the existing resources, by using the functions hasAssertion or GetAllResources of the interface nsIRDFDataSource.
(Next) Next, we'll see how to access the system clipboard for copy and paste operations.