Re-using Classic Domino Outlines, Rough Draft

Tue Jun 19 17:34:00 EDT 2012

Tags: xpages

My current big project at work involves, among many other things, viewing individual project databases through a centralized "portal". These all use the same base template, but can be individually customized, usually with new or changed views. Additionally, I'm going to be granting web users varying roles that correspond to existing or planned access roles in the target database. The result is that I'm spending a good amount of time trying to dynamically adapt some classic Notes elements to show up on XPages (hence my DynamicViewCustomizer, which I've since updated and should post).

In addition to views, I wanted to use the existing Outline design elements in the databases, since they do a fine job and there's no reason to re-invent the wheel if it's not necessary. I looked around a bit and didn't see a way to readily re-use them (though I can't rule out the possibility that such a capability exists somewhere), so I decided I'd roll up my sleeves and write my own adapter.

The route I took was to use the xe:beanTreeNode ExtLib control inside the xe:navigator used in most OneUI apps. Though what I conceptually wanted to do was to feed it a tree structure made up of JavaScript arrays and nodes, that's not how it works: you pass it the name of either an already-created managed bean or the full class name of a bean-compatible (read: zero-argument constructor) implementing ITreeNode. Presumably, the right way to do this would be to create a managed bean and pass in configuration parameters via managed-property values. I didn't do that for the time being, though, instead baking a bit of environmental awareness into the Java code - so sue me. I also print stack traces to the server console, which you shouldn't do.

In any event, what my code does is to open the consistently-named outline design element (in this case, "TempOut" for "template outline"), reads through its entries, and recursively builds the node tree. There are a couple environmental assumptions in there, primarily in the constructor, in order to work with the surrounding app, but the core of it should be pretty transportable. One big note is that I haven't gotten around to adding in the "special" outline entries to show the remaining views and folders, nor do I handle "action" entries. Still, it may be a useful reference for using bean tree nodes.

package mcl;

import com.ibm.xsp.extlib.tree.ITreeNode;
import com.ibm.xsp.extlib.tree.impl.*;
import lotus.domino.*;

import javax.faces.context.FacesContext;
import java.net.URLEncoder;

public class ProjectOutline extends BasicNodeList {
	private static final long serialVersionUID = 2035066391725854740L;
	
	private String contextQueryString;
	private String dbURLPrefix;
	private String viewName;

	public ProjectOutline() throws NotesException {
		try {
			ProjectInfo projectDatabase = (ProjectInfo)JSFUtil.getVariableValue("projectDatabase");
			if(projectDatabase.isValidDB()) {
				Outline tempOut = projectDatabase.getOutline("TempOut");
			
				dbURLPrefix = "/__" + projectDatabase.getReplicaID() + ".nsf/";
				contextQueryString = String.valueOf(JSFUtil.getVariableValue("contextQueryString"));
				viewName = (String)FacesContext.getCurrentInstance().getExternalContext().getRequestParameterMap().get("viewName");
				
				OutlineEntry entry = tempOut.getFirst();
				while(entry != null) {
					entry = processNode(projectDatabase, tempOut, entry, null);
				}
			} else {
				addChild(new BasicLeafTreeNode());
			}
		} catch(NullPointerException npe) {
			addChild(new BasicLeafTreeNode());
		} catch(Exception e) {
			e.printStackTrace();
		}
	}
	
	
	private OutlineEntry processNode(ProjectInfo projectDatabase, Outline outline, OutlineEntry entry, BasicContainerTreeNode root) throws NotesException {
		int level = entry.getLevel();
		
		switch(entry.getType()) {
		case OutlineEntry.OUTLINE_OTHER_UNKNOWN_TYPE:
			// Must be a container
			
			BasicContainerTreeNode containerNode = createSectionNode(entry);
			if(root == null) {
				addChild(containerNode);
			} else {
				root.addChild(containerNode);
			}
			// Look for its children
			OutlineEntry nextEntry = outline.getNext(entry);
			while(nextEntry.getLevel() > level) {
				nextEntry = processNode(projectDatabase, outline, nextEntry, containerNode);
			}
			
			return nextEntry;
		case OutlineEntry.OUTLINE_TYPE_NAMEDELEMENT:
			View view = projectDatabase.getView(entry.getNamedElement());
			if(view != null) {
				ITreeNode leafNode = createViewNode(entry);
				if(root == null) {
					addChild(leafNode);
				} else {
					root.addChild(leafNode);
				}
				view.recycle();
			}
			
			return outline.getNext(entry);
		}
		
		return outline.getNext(entry);
	}
	
	private BasicContainerTreeNode createSectionNode(OutlineEntry entry) throws NotesException {
		BasicContainerTreeNode node = new BasicContainerTreeNode();
		node.setLabel(entry.getLabel());
		node.setImage(dbURLPrefix + urlEncode(entry.getImagesText()) + "?Open&ImgIndex=1");
		
		return node;
	}
	private ITreeNode createViewNode(OutlineEntry entry) throws NotesException {
		BasicLeafTreeNode node = new BasicLeafTreeNode();
		node.setLabel(entry.getLabel());
		node.setImage(dbURLPrefix + urlEncode(entry.getImagesText()) + "?Open&ImgIndex=1");
		node.setHref("/Project_View.xsp?" + contextQueryString + "&viewName=" + urlEncode(entry.getNamedElement()));
		
		node.setSelected(entry.getNamedElement().equals(viewName));
		node.setRendered(resolveHideFormula(entry));
		
		return node;
	}

	private String urlEncode(String value) {
		try {
			return URLEncoder.encode(value, "UTF-8");
		} catch(Exception e) { return value; }
	}
	private boolean resolveHideFormula(OutlineEntry entry) throws NotesException {
		String hideFormula = entry.getHideFormula();
		if(hideFormula != null && hideFormula.length() > 0) {
			Session session = JSFUtil.getSession();
			Database database = entry.getParent().getParentDatabase();
			Document contextDoc = database.createDocument();
			// @UserAccess gave me trouble, so I just did a simple string replacement, since I know that's the only way I used it
			hideFormula = hideFormula.replace("@UserAccess(@DbName; [AccessLevel])", "\"" + String.valueOf(database.getCurrentAccessLevel()) + "\"");
			double result = (Double)session.evaluate(hideFormula, contextDoc).get(0);
			contextDoc.recycle();
			return result != 1;
		}
		return true;
	}
}
New Comment