The driving force behind my current Javascript experiment was a need to modernize the UI for the Pivot Table. Clearly the old good JPivot-like UI doesn't match modern users expectations: context menus, drag & drop items, etc. So, once my Javascript cellset fulfilled basic functionality (drill up/down and add/remove hierarchies) I've started my work to rejuvenate the UI into a more stylish one.
An Experiment: Using Javascript to Display CellSet Data
I've just started to use Javascript professionally so I've decided to apply my new knowledge to experiment on using this technology to display MDX data. My goal was restricted to display a CellSetAxis from a fixed query, allowing the user to drill down/drill up on any member.
Version 7: Added Support for Level Selections
Live Demo updated with this version
Source code can be found here
My goal for this release was adding support for level selections in QueryHierarchy. I do know it wouldn't be easy, but it was harder than expected. So this version implements a UI rather clumsy to include/exclude levels from a QueryHierarchy: I've added a Level option to the operator selector that lets you include or exclude a level by acting upon any member of that level. The following screenshot shows the query hierarchy editor for the [Store] hierarchy after including the [Store].[Store Country] and [Store].[Store City] hierarchies. And the resulting cellset table after collapsing [Store].[Canada]
Version 6: Hierarchy Expansion, let’s drill-up
This version includes the capability to expand/collapse a hierarchy in the cellset table. An expanded hierarchy initially shows all of its members, and the user can drill-up to hide details. A collapsed hierarchy initially shows only its root members, and the user can drill-down to show further detail.
![]() |
| Expanded Hierarchy: Store Type hierarchy expanded with some undrilled members |
This functionality is supported by three methods in the QueryAxis class:
- void expandHierarchy(QueryHierarchy h)Expands the hierarchy, invalidating any previous drill/undrill operation on every appearance of h in this axis
- void collapseHierarchy(QueryHierarchy h)
Collapses the hierarchy, invalidating any previous drill/undrill operation on every appearance of h in this axis - boolean isExpanded(QueryHierarchy h)
Tests if the provided hierarchy is expanded in this axis.
QueryHierarchy Expression Generation
The strategy to MDX generation for an expanded hierarchy is as follows:
- A fully expanded hierarchy generates this MDX:
DESCENDANTS(<root members set>, 0, SELF_AND_AFTER) - Member undrill is implemented by an external EXCEPT:
EXCEPT(
DESCENDANTS(<root members set>, 0, SELF_AND_AFTER),
DESCENDANTS(<undrilled members set>, 0, AFTER)
)
Refactoring Axis Set Expression Generation
/**
* Recursively generates the set expression for a Query Hierarchy.
*
* @param current
* currently visited node
* @param expander
* helper object to execute drills/expansions
* @param drillList
* members to drill/undrill
* @param expression
* generated expression
*/
@SuppressWarnings("unused")
private void toOlap4jQueryDidactic(VisitingInfo current,
HierarchyExpander expander, List<Member> drillList,
AxisExpression expression) {
boolean isMemberIncluded =
current.getEffectiveSign(Operator.MEMBER) == Sign.INCLUDE;
boolean areChildrenIncluded =
current.getEffectiveSign(Operator.CHILDREN) == Sign.INCLUDE;
boolean areDescendantsIncluded =
current.getEffectiveSign(Operator.DESCENDANTS) == Sign.INCLUDE;
// Processes current member, including or excluding it from the
// expression as necessary.
Member currentMember = current.getMember();
if (isMemberIncluded) {
expression.include(currentMember);
if (expander.isDrilled(currentMember, drillList)) {
// Current member is included in the hierarchy but collapsed.
// Ends this visit as we should not include any descendant.
return;
}
}
// Recursively calls toOlap4jQueryDidactic on overrided children
List<Member> overridedChildren = new ArrayList<Member>();
for (SelectionTree overridedChild : current.getNode()
.getOverridingChildren()) {
overridedChildren.add(overridedChild.getMember());
VisitingInfo childVisit = current.visitChild(overridedChild);
toOlap4jQueryDidactic(
childVisit,
expander,
drillList,
expression);
}
if (areDescendantsIncluded) {
// Expand/undrill descendants
MemberSet expansionBase;
if (areChildrenIncluded) {
expansionBase = new ChildrenMemberSet(currentMember,
overridedChildren);
} else {
expansionBase = new GrandchildrenSet(currentMember,
overridedChildren);
}
expander.expand(expansionBase, drillList, expression);
} else {
// Include children if necessary
if (areChildrenIncluded) {
MemberSet nonOverridingChildren
= new ChildrenMemberSet(currentMember, overridedChildren);
expression.include(nonOverridingChildren.getMdx());
}
}
}
}
Version 4: Filtering Capabilities
![]() |
| Screenshot of the sample application showing a filtered query and filter editor. |
So I've decide to change the behavior of some QueryHierarchy's methods when the instance lives in a slicer axis (hierarchy.geAxis().getLocation() == Axis.FILTER):
- include(Member m, Operator op), exclude(Member m, Operator op)Both methods ignore the op argument and assume Operator.DESCENDANTS instead.
- isIncluded(Member m)Returns true if and only if m and all of its descendants are included.
- isExcluded(Member m)
(new)
Returns true if and only if m and all of its descendants are excluded. Equivalente to !isIncluded(m) for instances living in query axes.
- The operator dropdown menu disappears
- The include/exclude buttons are replaced by tri-state checkbox buttons, checked when the member is included, unchecked when it's excluded, or mixed if neither isIncluded(m)
nor isExcluded(m) return true.
MDX Generation for WHERE clause
The package private method toOlap4j in the QueryAxis class delegates in the new toOlap4jFilter() method when generating the MDX expression for a slicer axis. This method generates a cross join of the expressions returned by the olap4jFilter() method for each hierarchy in the axis:private AxisNode toOlap4jFilter() {
CrossJoinBuilder xJoin = new CrossJoinBuilder();
for(QueryHierarchy h : hierarchies) {
xJoin.join(h.toOlap4jFilter());
}
return
new AxisNode(null, false, axis, null, xJoin.getJoinNode());
}
The MDX expression for a silcer QueryHierarchy is computed recursively based on the tree of included/excluded members:
- Simple case: current node has no overriding children.The resulting expression is the MemberNode for the current member if it's included, void expression if it's not included.
- Recursive case: current node has overriding children.
- If the current node is excluded, return the union of the recursive expressions for every overriding children
- If the current node is included, return the union of
- computing the set of non-overriding children as EXCEPT(<currentNode>.CHILDREN, <set of overriding children>)
- computing the union of the recursive expression for every overriding children
- computing the set of non-overriding children as EXCEPT(<currentNode>.CHILDREN, <set of overriding children>)
- If the current node is excluded, return the union of the recursive expressions for every overriding children
private
ParseTreeNode toOlap4jFilter(SelectionTree selectionNode,
Sign defaultSign) {
Sign selectionSign = selectionNode.getStatus().getEffectiveSign(
Operator.DESCENDANTS, defaultSign);
if (!selectionNode.hasOverridingChildren()) {
// Current node has no overriding children, its filter expression is
// the corresponding MemberNode if the member is included, void in
// other case.
if (selectionSign == Sign.INCLUDE)
return Mdx.member(selectionNode.getMember());
else
return
null;
} else {
// Current node has overriding children
UnionBuilder finalExpression = new UnionBuilder();
if (selectionSign == Sign.INCLUDE) {
// Current node is included, so overriding children are excluded
// or have excluded descendants.
UnionBuilder overridingChildren = new UnionBuilder();
for (SelectionTree overriding : selectionNode
.getOverridingChildren()) {
overridingChildren.add(Mdx.member(overriding.getMember()));
finalExpression.add(toOlap4jFilter(overriding,
selectionSign));
}
// Return the set of non overriding children plus recursive
// expression evaluations
finalExpression.add(Mdx.except(
Mdx.children(selectionNode.getMember()),
overridingChildren));
} else {
// Current node is excluded, returns the union of recursive
// evaluation for overriding children.
for (SelectionTree overriding : selectionNode
.getOverridingChildren()) {
finalExpression.add(toOlap4jFilter(overriding,
selectionSign));
}
}
return finalExpression.getUnionNode();
}
}
FilterAxisInfo Composite Faces Component
The component to display the filter axis for the current query hides a standard <h:dataTable> and relays on a managed bean to compute the filter expression to generate the textual description of the filter and adapt the call to remove a hierarchy from the filter axis. This component has the following attributes:- value (instance of AbstractQueryBean)
The query containing the axis to display (used to remove a hierarchy from the axis) - style, styleClass
CSS style and CSS style class to be applied to the component - headerClassCSS style class to be applied to the column showing the hierarchy captions.
- expressionClassCSS style class to be applied to the column showing the filter descriptions.
<h:panelGroup
id="filterAxisInfo"
>
<olap:filterAxisInfo
value="#{queryBean}">
<f:ajax
render=":form:filterAxisInfo :form:table :form:hierarchyEditor"/>
</olap:filterAxisInfo>
</h:panelGroup>
It uses default style classes and uses the client event to re-render itself, the result set table and the query hierarchy editor after filter hierarchy removal.
Version 3: Part I, Drilling CellSet Encapsulation
First of all: Happy New Year to everybody!!
During this holidays I've working on implementing capabilities visually edit a query: adding/removing of hierarchies and inclusion/exclusion of members of those hierarchies. I'll explain you my achievements in two blogs entries: this one explains the AbstractQueryBean and its usage to provide an easy to use drillable cellset table, the next one will explain components allowing selection of hierarchies and inclusion/exclusion of its members.
Anyway, for the impatient among us, this is a screenshot of "version 3", and a link to the source code.
An Abstract Managed Bean to Handle Query State
The version in my previous blog entry used a helper class to serialize/deserialize the Query instance, and put it explicitly into the ViewState. This strategy works but has two main drawbacks: it's not easily extensible, and the name used to store the serialized query into the view state can silently clash with the names of other objects.After some refactoring cycles I've ended up with a better solution: use a ViewScoped managed bean to manage the state of the query, and use the standard Java serialization mechanism to save and restore that state. This bean will handle connection management, needed to support the query deserialization; and caching for the query CellSet result.
To accomplish the vision of an easily pluggable component library I've added an abstract class AbstractQueryBean intended to serve as a base class for concrete view
scoped managed beans. This class defines two abstract methods to be implemented by deriving classes:
- OlapConnection initConnection()Classes derived from AbstractQueryBean must implement this method to return a connection to the olap4j provider.
- Query initQuery()Classes derived from AbstractQueryBean must implement this method to return newly a initialized query.
As I've pointed before, this class provides also with caching for the resulting CellSet from the query. The method getCellSet() tests for a cached CellSet before executing the current query. Convenience methods included in this class altering the query state invalidate this cache, to force a query execution in the next call to getCellSet(). And there is a invalidateCellSet() method to force this cache invalidation if the user of the class modifies externally the state of the query.
I'll change this manual cache invalidation mechanism and replace it with another based on QueryListener as the one used in the original query package.
Follows a minimal example of how to use this class to create a managed bean to serve as backing bean for drillable cellset:
package olaptest;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import javax.faces.bean.ManagedBean;
import javax.faces.bean.ViewScoped;
import org.olap4j.Axis;
import org.olap4j.OlapConnection;
import org.olap4j.OlapException;
import org.olap4j.mdx.IdentifierNode;
import org.olap4j.metadata.Cube;
import es.cgalesanco.faces.olap4j.managedBeans.AbstractQueryBean;
import es.cgalesanco.olap4j.query.Query;
import es.cgalesanco.olap4j.query.QueryAxis;
import es.cgalesanco.olap4j.query.QueryHierarchy;
import es.cgalesanco.olap4j.query.Selection.Operator;
@ManagedBean
@ViewScoped // View scoped to save the state of the query between requests
public
class SampleQueryBean extends AbstractQueryBean{
private static final long serialVersionUID = 1L;
@Override
protected OlapConnection initConnection() throws OlapException {
…
Connection jdbcCn = DriverManager.getConnection(…);
…
return jdbcCn.unwrap(OlapConnection.class);
}
@Override
protected Query initQuery() throws OlapException {
Cube c = getConnection().getOlapSchema().getCubes().get("Sales");
Query query = new Query("MyQuery", c);
QueryAxis columnsAxis = query.getAxis(Axis.COLUMNS);
columnsAxis.setNonEmpty(true);
columnsAxis.addHierarchy(query.getHierarchy("Gender"));
…
return query;
}
}
Composite Control To Encapsulate Drilling CellSet
Insisting on easy construction of olap4j/JSF applications I've encapsulated the previous sample of a drillable cellset into a composite component using the AbstractQueryBean. This version of the composite components adds functionality to remove a hierarchy from the query by clicking on a close button in the hierarchy header cell.So, the following code snippet, includes a drillable cellset table using the previous sample query bean:
<olap:queryCellSet id="table" query="#{sampleQueryBean}">
Yeah, that's all folks. And, a slightly more complicated version using AJAX to update the table on drills and hierarchy removals
<h:form id="form">
…
<olap:queryCellSet id="table" query="#{queryBean}">
<f:ajax event="drill" render=":form:table"/>
<f:ajax event="axisChange" render=":form:table"/>
</olap:queryCellSet>
Version 1: Drill-Enabled CellSet Table
Adding Drill Buttons
Let's start modifying the contents in the <olap:cellSetAxisforAxis="rows"/>. I'll add a <h:commandButton/> to let the user drill/undrill a member in the cellset table. This button must be rendered only if the member has children, and will show a '-' if the member is already drilled or a '+' otherwise. This is the corresponding Facelet markup.
<span
style="padding-left: #{m.member.depth}ex">
<h:commandButton
rendered="#{m.member.childMemberCount > 0}"
value="#{olapSample.isDrilled(component,m.position) ? '-' : '+'}"
/>
action="#{olapSample.toggleDrill(component,m.position)}"
<h:outputText
value="#{m.member.caption}"
/>
</span>
To support this markup we'll add two methods to our managed bean:
- boolean isDrilled(UIComponent source, List<Member> position);This method receives a component and a positioned member, and returns a boolean value indicating if that member is drilled or not.
- boolean toggleDrill(UIComponent source, List<Member> position);This method receives a component and a positioned member, and modifies the current query to change the drill status of the positioned member.
This is the code snippet for toggleDrill
public
void toggleDrill(UIComponent c, List<Member> position)
throws OlapException, SQLException {
// Find the UICellSetAxis within the ancestors of 'c'
while (c != null && !(c instanceof UICellSetAxis)) {
c = c.getParent();
}
if (c == null)
return;
// Get the query axis based on the UICellSetAxis information
final UICellSetAxis axisComponent = (UICellSetAxis) c;
QueryAxis queryAxis = getQuery().getAxis(
axisComponent.getCellSetAxis().getAxisOrdinal());
// Toggle drill
Member[] members = position.toArray(new Member[position.size()]);
if (queryAxis.isDrilled(members))
queryAxis.undrill(members);
else
queryAxis.drill(members);
// Invalidate the CellSet caché
cs = null;
}
The isDrilled method has a similar structure.
The only point remaining to be explained is the getQuery call, it's related to the query state saving strategy.
Query State Saving Strategy
The OlapSample managed bean is a request-scoped bean, so it doesn't keep state between requests. But, to make the table functional, we need to keep the drill state of current query between requests. So I'll save the query state in the ViewState. This would be the code:Map<String, Object> viewState = FacesContext.getCurrentInstance().getViewRoot().getViewMap();
// Put the query in the View State
viewState.put("SavedQuery", query);
// Get the query from the View State
query = (Query)viewState.get("SavedQuery");
Unfortunately it won't work… Query instances cannot be serialized (mainly because they contain references to database connections), so they can't be added to the ViewState; I need helper class to create a serializable object from a Query instance and to reconstruct the original query from that object. That class, QuerySaver, has to static public methods:
- Object saveQuery(Query q)Generates the serializable object from the query. It will be an array of objects containing the names of the query, the source cube, axis dimensions, drilled members, etc.
- Query restoreQuery(OlapConnection cn, Object state)This method receives an object produced by saveQuery and an OlapConnection and recreates the original query.
private Query getQuery() throws OlapException {
if (query != null)
return
query;
Map<String, Object> viewMap = FacesContext.getCurrentInstance()
.getViewRoot().getViewMap();
Object savedQuery = viewMap.get("SavedQuery");
if (savedQuery == null) {
// There is no saved query, so we create the query used to
// show our initial CellSet.
query = initQuery();
} else {
query = QuerySaver.restoreQuery(getConnection(), savedQuery);
}
return
query;
}
I'm going to save the query state just before rendering the cellset. To make this I'll use attach a listener method to the preRenderComponent event for the <olap:cellSetTable>
<olap:cellSetTable
value="#{olapSample2.sampleCellSet}" …>
<f:event
type="preRenderComponent"
listener="#{olapSample.saveQuery}"/>
…
<olap:cellSetTable/>
Version 0: Rendering a CellSet
The focus for this initial version is about rendering an arbitrary cellset result. In the previous post I proposed a component structure with a main UICellSet component acting as a container of two UICellSetAxis component instances, one for each CellSetAxis, and a UICellSetCells component to handle the data cells.
HtmlCellSetRenderer
|
|
HtmlRowsAxisRenderer
|
|
HtmlColumnsAxisRenderer
|
|
HtmlCellSetCellsRenderer
|




