Monday, March 30, 2009

Filtering Bookmarks by Tags in Your Extension

It is not possible to filter by multiple tags in bookmarks window in Firefox. So I thought to try to see how it can be done. This exercise is interesting too to see how templates work and how to change the query for template in runtime. I prepared a XUL window that is just enough to demonstrate the point.
First of all get to know Places, the heart of the Firefox's bookmarks and history management system. The Places database is where Firefox keeps all it's records on your bookmarks and history, and it is simply SQLite db/file. Here's the schema and the database itself can be found at Firefox's profiles folder, the file is named places.sqlite. You can view it using SQLite Database Browser.
With all the links out of the way lets look at the example file:


<?xml version="1.0"?>
<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
xmlns:html="http://www.w3.org/1999/xhtml"
title="Bookmarks - Test"
>

<script type="application/x-javascript">

var someSelectionQuery =
'select p.title as title ' +
'from moz_bookmarks_roots r ' +
'join moz_bookmarks tagsRoot on r.folder_id = tagsRoot.id ' +
'join moz_bookmarks tags on tagsRoot.id = tags.parent ' +
'join moz_bookmarks b on b.parent = tags.id ' +
'join moz_places p on b.fk = p.id ' +
"where r.root_name = 'tags' and tags.title in ({tags}) " +
'group by b.fk ' +
'having count(b.fk) = {count}';

function doOnSelect(tagList){
var items = tagList.selectedItems;
var count = tagList.selectedCount;
var query = document.getElementById('bookmarkslistQuery');

var tags = '';
var countStr = count + '';

for(var i=0; i &lt; count; i++){
if(i != 0) tags += ',';
tags += "'" + tagList.selectedItems[i].label + "'";
}

var realQuery = someSelectionQuery.replace('{tags}', tags);
realQuery = realQuery.replace('{count}', countStr);

query.textContent = realQuery;

var ml = document.getElementById('mainList');
ml.builder.rebuild();
}
</script>

<listbox datasources="profile:places.sqlite" ref="*" querytype="storage" seltype="multiple" id="tagsList" onselect="doOnSelect(this);">
<template>
<query>
select tags.title
from moz_bookmarks_roots r
join moz_bookmarks tagsRoot on r.folder_id = tagsRoot.id
join moz_bookmarks tags on tagsRoot.id = tags.parent
where r.root_name = 'tags'
</query>
<action>
<listitem uri="?" label="?title"/>
</action>
</template>
</listbox>

<listbox datasources="profile:places.sqlite" ref="*" querytype="storage" id="mainList">
<template>
<query id="bookmarkslistQuery">
select b.title as title
from moz_bookmarks_roots r
join moz_bookmarks tagsRoot on r.folder_id = tagsRoot.id
join moz_bookmarks tags on tagsRoot.id = tags.parent
join moz_bookmarks b on b.parent = tags.id
</query>
<action id="blaction">
<listitem uri="?" label="?title"/>
</action>
</template>
</listbox>

</window>



OK, lets quickly go through this. Lets skip the JavaScript part at the beginning, at the end there are two listbox elements that have templates. The first listbox is showing a list of your tags. It's very simple SQLite template. Datasource attribute defines the datasource on which the query will be executed. In this case the file is profile:places.sqlite, and that is Places database, other required attribute is querytype which is marking that our datasource is a SQLite file. We have very simple template that has only query and action elements. Query is just a SQLite query, simple, once you figure out what is where in the database. And template is using tag titles as labels in listitem. What it will do is just render a list of tags, and by selecting tags bookmarks will be filtered.
The second listbox is rendering all tagged bookmarks, that's just to show something until a tag is selected. The label is name of bookmarked page. It is pretty much the same thing as with tags listbox. It is going to be a bit different though, since the query is going to be changed dynamically.
So finally the JavaScript. doOnSelect function is doing all the work (well there's no other function there). On selection of tag, multiple can be selected, the function is just making a list of tag names separated by commas, updating the someSelectionQuery to include filter on tags. There is a little trick on group by and having - we're just making sure that particular place has the exact number of occurences as the number of tags selected. Basically all selected tags have to be parents of the bookmarked page, so that is what the query is checking. Oh, I forgot to mention Places SQL queries best practices.
At the end of the function we need to call builder.rebuild() on the templated element to update the content since the query is changed, it won't be updated by itself.
Phew, lots of work for such an ugly screen.

Sunday, March 8, 2009

Setup Your Virtual Folder During Deployment

After manually setting http handlers for a particular folder in IIS 5 and 6 and then seeing how it can be done programmatically we are ready to use it in our deployment.
It is quite simple actually. Here are the things we need to do:

1. Create web setup project
2. Create custom installer action (it's in VB though, but it is simple enough)
3. Override Commit method of the created installer class
4. Add your action to be executed in the commit phase of the setup

Steps 1 and 2 should be straight forward, however steps 3 and 4 require some more explanation. First of all the web setup project exposes two useful properties through Installation Address User Interface Dialog Box (that's just where you select your site and virtual folder): TARGETSITE and TARGETVDIR. These are as their names suggest: site (its metabase value) and virtual folder where the deployment will occur. That simplifies our life in a way that we can get a reference to our virtual folder's metabase entry like this:


webSiteId = Context.Parameters["TARGETSITE"];
webSiteId = webSiteId.Substring(webSiteId.LastIndexOf('/') + 1);

virtualDirectory = Context.Parameters["TARGETVDIR"];

var myVirtualDir = new System.DirectoryServices.DirectoryEntry(
"IIS://localhost/W3SVC/" + webSiteId +"/ROOT/" + virtualDirectory);

Piece of cake right?
Now, what have we done is get TARGETSITE parameter, and we took just the site's id (because the format doesn't exactly fit), we used TARGETVDIR and composed the path we can use in DirectoryEntry constructor. After getting the directory entry you just do your configuration magic.
There is one piece of the puzzle missing - how do we get those context parameters into our custom installer action? As the documentation says the parameters are available, however we still need to pass them into our action as part of the custom action data. Nothing easier:



Or in plain text: /TARGETVDIR="[TARGETVDIR]" /TARGETSITE="[TARGETSITE]"
And at the end, the answer to Why do we do this in the commit phase? Because the web setup will overwrite anything you change before the commit phase. It is creating the virtual folder, or setting default configuration to it if it already exists, in the commit phase. This means that not before the commit phase that you have the correct starting point for your changes.

So to conclude: nothing ground breaking, just lots of simple little steps that you cannot really quickly figure out from confusing MSDN; unless, of course you are making your living out of deployment projects and you knew all this already.