Thursday, July 16, 2009

Document Management in NetCenter

Although our mid to long term plans for NetCenter365 include Sharepoint and Alfresco integration, we currently provide a more streamlined, account oriented, document management capability within NetCenter that we think might better serve some organizations.

Documents in NetCenter are attached to customer records or accounts. Here's a screenshot:


On the backend, I created a C++/FUSE based filesystem. When you mount it you see a list of customer names as directories, under which documents attached to the accounts are found. This metadata is stored in the NetCenter database while the actual file contents are simply stored in a backing ext3 filesystem. This way it's easy to backup and restore, replicate, etc. Here's a snippet from account_node::readdir()
 int account_node::readdir(void *buf, fuse_fill_dir_t filler, off_t offset, struct fuse_file_info *fi)
{
filler(buf, ".", NULL, 0);
filler(buf, "..", NULL, 0);

pqxx::connection db(connect_string());
pqxx::nontransaction work(db);
pqxx::result result = work.exec("SELECT name,id,trunc(date_part('epoch',last_updated)),path FROM document where account_id=" + id());

std::string did; long lctm; std::string rpath;
for (pqxx::result::const_iterator r = result.begin(); r != result.end(); ++r)
{
filler(buf, r[0].c_str(), NULL, 0);
did = r[1].c_str();
r[2].to(lctm);
rpath = r[3].c_str();

std::string path = _path + "/" + r[0].c_str();
_filesystem->set_attributes(path, attributes(did, lctm, rpath));
}

return 0;
}
Whereas the code to read the actual file contents, looks something like this:
 int poi_node::open(struct fuse_file_info *fi)
{
std::string fpath = full_path();

int res = ::open(fpath.c_str(), fi->flags);
if (res == -1)
return -errno;

::close(res);
return 0;
}
With the virtual filesystem mounted, we simply serve it up via Apache webdav and since we store the document metadata in the NetCenter database it's very easy to provide the frontend UI via grails.

As far as the frontend goes, one big complaint we've heard about other document management solutions is how confusing it is for some users to download a file, find it on their hard drive, edit it, go back to their browser, and upload a new version. That's a very frustrating set of steps for many users.

We built a very simple JetPack based extension for Firefox that registers a "webdav://" protocol handler that passes off such links to OpenOffice which already knows how to handle them properly such that there is no downloading, finding, editing, and re-uploading. OpenOffice will directly save the document back to our Apache webdav server that sits on top of the NetCenter virtual filesystem discussed above.

For Internet Explorer, we wrote a small C# based protocol handler that does almost the same thing but handles Microsoft Word or OpenOffice. Not quite as nice as the Firefox solution, but we can push out the MSI via AD group policy.

Tuesday, July 14, 2009

Grails, jQuery, and Yahoo Maps

I recently completed a new NetCenter365 feature that uses Yahoo Maps to show the location of all current customers. Here's a screenshot:


I really appreciate Yahoo's "Maps Web Services" which include a helpful geolocation service.

First, we map out HQ with:
 var map = new YMap(document.getElementById('map'));
map.addTypeControl(); map.addZoomLong(); map.addPanControl();
map.setMapType(YAHOO_MAP_REG);

var hq = new YGeoPoint(HQ.latitude, HQ.longitude);
map.drawZoomAndCenter(hq, 11);
Then we use grails and jquery to loop through every customer and fire off the following ajax requests:
 var url = '${createLink(controller: "location", action: "latlong")}' + "/";

<g:each var="account" in="${accounts}">
$.getJSON(url + ${account.id}, function(x) {
var pt = new YGeoPoint(x.latitude, x.longitude);
var m = new YMarker(pt);
m.addAutoExpand('${account.name.encodeAsJavaScript()}');
map.addOverlay(m);
});
</g:each>
The heart of the location/latlong method uses Yahoo's geolocation services. Here's a snippet of the groovy code:
 def geocoder = "http://local.yahooapis.com/MapsService/V1/geocode?appid=${APPID}"
if (account.line1) geocoder += "&street=" + URLEncoder.encode(account.line1);
if (account.city) geocoder += "&city=" + URLEncoder.encode(account.city);
if (account.state) geocoder += "&state=" + account.state;
if (account.zip) geocoder += "&zip=" + account.zip;

def xml = geocoder.toURL().text
def records = new XmlParser().parseText(xml);
location.latitude = records.Result[0].Latitude.text()
location.longitude = records.Result[0].Longitude.text()
Performance wise, the map pops up quite quickly and the markers appear in rapid procession. This is aided by caching Lat/Long info to minimize geolocation requests.