I've just set installed a live demo running the current SNAPSHOT version of the project.
http://demo.desarrolloagil.es/olap-faces/test.jsf
Version 6: Hierarchy Expansion, let’s drill-up
Wednesday, February 22, 2012
You can download this version of the sample, and an improved version of the component library from here.
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.
I've modified the queryCellSet composite component (and QueryCellSetBean, its associated managed bean) to include a hierarchy expansion button in the header of both cellset axes, allowing the user toggling the expand state for a hierarchy.
This functionality is supported by three methods in the QueryAxis class:
QueryHierarchy Expression Generation
The strategy to MDX generation for an expanded hierarchy is as follows:
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());
}
}
}
}
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
Previously, axis MDX expression generation was driven by the drill tree. I've refactored this logic to let the axis expression generation be driven by the QueryHierarchy inclusion/exclusion tree.
This is a simplified version of the function to generate an axis set expression for a QueryHierarchy/**
* 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 5: Refining Hierarchy Selection UI and Query Axis Editing
Thursday, February 02, 2012
You can download this version of the sample, and an improved version of the component library from here.
![]() |
| Sample page including the <olap:queryAxisEditor> component |
- Displays the current cube's hierarchies organized by the axis where they are used. Unused hierarchies are displayed in an "Unused"
axis. - Allows for hierarchy sorting (within the ROWS and COLUMN axes).
- Allows hierarchy removal from an axis.
- Allows moving used hierarchies to other query axes.
- Keeps a selected hierarchy (in the sample page this selected hierarchy is the one displayed by the query hierarchy editor)
- Includes a button to swap rows and columns axes.
- Allows selecting of the non-empty state for ROWS and COLUMNS axes.
<olap:queryAxisEditor
id="axisEditor"
query="#{queryBean}"
value="#{queryBean.selectedHierarchyName}">
id="axisEditor"
query="#{queryBean}"
value="#{queryBean.selectedHierarchyName}">
<f:ajax
event="change"
execute=":form:axisEditor"
render=":form:axisEditor :form:hierarchyEditor"/>
event="change"
execute=":form:axisEditor"
render=":form:axisEditor :form:hierarchyEditor"/>
<f:ajax
event="edit"
render="@all"/>
event="edit"
render="@all"/>
</olap:queryAxisEditor>
<olap:queryAxisEditor> Composite Component Internals
The queryAxisEditor composite component takes the following parameters:
- query: AbstractQueryBean derived managed bean keeping the query state
- value: backing bean property used to keep the current selected hierarchy name
- change: when the user changes the currently selected hierarchy
- edit: when the user clicks on any action button (remove, add to rows, add to columns, add to filter, push up or push down) changing the current query.
<composite:interface>
<composite:attribute
name="query"
/>
name="query"
/>
<composite:attribute
name="value"
/>
name="value"
/>
<composite:attribute
name="styleClass"
/>
name="styleClass"
/>
<composite:attribute
name="headerClass"
/>
name="headerClass"
/>
<composite:attribute
name="rowClasses"
/>
name="rowClasses"
/>
<composite:clientBehavior
name="change"
default="true"
event="click"
targets="columns rows filter unused"
/>
name="change"
default="true"
event="click"
targets="columns rows filter unused"
/>
<composite:clientBehavior
name="edit"
event="edit"
targets="columns rows filter unused"
/>
name="edit"
event="edit"
targets="columns rows filter unused"
/>
<composite:clientBehavior
name="edit"
targets="swap"
/>
name="edit"
targets="swap"
/>
</composite:interface>
<composite:implementation>
<div
id="#{cc.attrs.clientId}">
id="#{cc.attrs.clientId}">
<h:commandButton
id="swap"
styleClass="cgaofHierarchyListSwapBtn"
value="Swap Axes"
action="#{queryAxisEditorBean.swapAxes(cc.attrs.query)}"/>
id="swap"
styleClass="cgaofHierarchyListSwapBtn"
value="Swap Axes"
action="#{queryAxisEditorBean.swapAxes(cc.attrs.query)}"/>
<h:inputHidden
id="selected"
value="#{cc.attrs.value}"/>
id="selected"
value="#{cc.attrs.value}"/>
<olap:queryAxisEditorBase
query="#{cc.attrs.query}"
value="#{cc.attrs.value}"
valueControl="#{cc.attrs.clientId}:selected"
axisName="COLUMNS"
id="columns"
query="#{cc.attrs.query}"
value="#{cc.attrs.value}"
valueControl="#{cc.attrs.clientId}:selected"
axisName="COLUMNS"
id="columns"
styleClass="#{cc.attrs.styleClass}"
headerClass="#{cc.attrs.headerClass}"
rowClasses="#{cc.attrs.rowClasses}"/>
headerClass="#{cc.attrs.headerClass}"
rowClasses="#{cc.attrs.rowClasses}"/>
<olap:queryAxisEditorBase
query="#{cc.attrs.query}"
value="#{cc.attrs.value}"
valueControl="#{cc.attrs.clientId}:selected"
axisName="ROWS"
id="rows"
query="#{cc.attrs.query}"
value="#{cc.attrs.value}"
valueControl="#{cc.attrs.clientId}:selected"
axisName="ROWS"
id="rows"
styleClass="#{cc.attrs.styleClass}"
headerClass="#{cc.attrs.headerClass}"
rowClasses="#{cc.attrs.rowClasses}"/>
headerClass="#{cc.attrs.headerClass}"
rowClasses="#{cc.attrs.rowClasses}"/>
<olap:queryAxisEditorBase
query="#{cc.attrs.query}"
value="#{cc.attrs.value}"
valueControl="#{cc.attrs.clientId}:selected"
axisName="FILTER"
id="filter"
query="#{cc.attrs.query}"
value="#{cc.attrs.value}"
valueControl="#{cc.attrs.clientId}:selected"
axisName="FILTER"
id="filter"
rendered="#{cc.attrs.query.slicerHierarchies.size() > 0}"
styleClass="#{cc.attrs.styleClass}"
styleClass="#{cc.attrs.styleClass}"
headerClass="#{cc.attrs.headerClass}"
rowClasses="#{cc.attrs.rowClasses}"/>
rowClasses="#{cc.attrs.rowClasses}"/>
<olap:queryAxisEditorBase
query="#{cc.attrs.query}"
value="#{cc.attrs.value}"
valueControl="#{cc.attrs.clientId}:selected"
id="unused"
query="#{cc.attrs.query}"
value="#{cc.attrs.value}"
valueControl="#{cc.attrs.clientId}:selected"
id="unused"
styleClass="#{cc.attrs.styleClass}"
headerClass="#{cc.attrs.headerClass}"
rowClasses="#{cc.attrs.rowClasses}"/>
headerClass="#{cc.attrs.headerClass}"
rowClasses="#{cc.attrs.rowClasses}"/>
</div>
</composite:implementation>
- The axis name and the non-empty toggle button are displayed within the table's header.
- Each hierarchy within the axis is displayed in a row with three columns:
- A column for the remove button.
- A column for the hierarchy's caption. The caption includes javascript to change the selected hierarchy on clicking.
- A column for the other action buttons: rows, columns, filter, push up and push down.
Required Query Package Improvements
The improvements implemented in the query package have been straightforward and all of them consisted in implementing methods already defined by the org.olap4j.query package:- Query.swapAxes to allow axes swapping
- QueryHierarchy.isNonEmpty(), QueryHierarchy.setNonEmpty() to check/set the flag controlling the generation of NON EMPTY axis.
- QueryHierarchy.pushUp(), QueryHierarchy.pushDown()to allow moving a hierarchy within an axis.
Version 4: Filtering Capabilities
Tuesday, January 24, 2012
You can download
this version of the sample, and an improved version of the component library
from here.
The logic used in previous versions to generate MDX expressions for query axis (ROWS, COLUMNS, CHAPTERS, etc.) cannot be used against the FILTER axis. One of the reasons for this is that it's based on the drill state of displayed members. Furthermore the semantics associated to those axes are completely different from the semantics of the slicer axis. Specifically the operator argument to include and exclude methods in the QueryHierarchy class has no sense in a slicer axis, as including a MEMBER will aggregate the measures of its DESCENDANTS, and is equivalent to aggregate all of its CHILDREN.
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):
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:
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();
}
}
<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.
![]() |
| 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 II, Struggling with the Editing Hierarchy User Interface
Wednesday, January 18, 2012
You can download this version of the sample, and an improved version of the component library from here.
After redesigning QueryDimension to allow grater member inclusion functionality, I had to design a user interface to allow the user to exploit that functionality. I started by implementing a generic (non olap related) tree faces component to display the hierarchy and allow member navigation. I considered using a RichFaces or MyFaces Tomahawk tree component, but I've decided to implement a simple one to keep the project as selfcontained as possible. I'm not going to detail its design, suffice to say that it acts as <h:dataTable> but with a hierarchical data model implemented by a nested <treeItems> component, responsible of finding the children of a given data item.
My problem was providing an easy to use interface to let the user execute, at least, six actions (include or exclude for each of the basic operators: MEMBER, CHILDREN, DESCENDANTS) on every member of the hierarchy. Those were considered options:
The color and font weight of the members label indicate if the member is actually included or not in the query hierarchy. The operator menu over the hierarchy tree allows the user to select the operator of the exclusion/inclusion and, finally, the button beside the members label allows the user to toggle inclusion/exclusion of that member using the selected operator. The image in the button tells the user if clicking the button will result in an inclusion or exclusion; and the small label for the button provides a reminder of the selected operator.
After redesigning QueryDimension to allow grater member inclusion functionality, I had to design a user interface to allow the user to exploit that functionality. I started by implementing a generic (non olap related) tree faces component to display the hierarchy and allow member navigation. I considered using a RichFaces or MyFaces Tomahawk tree component, but I've decided to implement a simple one to keep the project as selfcontained as possible. I'm not going to detail its design, suffice to say that it acts as <h:dataTable> but with a hierarchical data model implemented by a nested <treeItems> component, responsible of finding the children of a given data item.
My problem was providing an easy to use interface to let the user execute, at least, six actions (include or exclude for each of the basic operators: MEMBER, CHILDREN, DESCENDANTS) on every member of the hierarchy. Those were considered options:
- A contextual menu for each member.
Probably the best solution but too complex, it will delay higher priority features (e.g.: filtering) - Three buttons to toggle inclusion for each operator.The simplest solution to implement, but so many buttons would clutter the UI.
- An operator selector, global for the whole component, plus one button to toggle each member inclusion/exclusion.The chosen solution. It allows every inclusion/exclusion operation allowed by QueryHierarchy and is "window-savvy".
The color and font weight of the members label indicate if the member is actually included or not in the query hierarchy. The operator menu over the hierarchy tree allows the user to select the operator of the exclusion/inclusion and, finally, the button beside the members label allows the user to toggle inclusion/exclusion of that member using the selected operator. The image in the button tells the user if clicking the button will result in an inclusion or exclusion; and the small label for the button provides a reminder of the selected operator.
Version 3: Part I, Drilling CellSet Encapsulation
Friday, January 13, 2012
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 2: Redesigning QueryDimension
Monday, December 19, 2011
You can download this version of the sample, and an improved version of the component library from here.
Once I've added drilling capabilities to QueryAxis, my next task has been redesigning QueryDimension member selection capabilities. My design goal was to improve the integration of this class with the UI components used to allow the user selecting the set of members to be included in the dimension. This goal pushed me to change the semantics associated to the include/exclude methods. The original olap4j implementation executes first all the includes and then all the excludes. My implementation executes the includes and excludes in the order they were invoked, so an hypothetical UI can include and exclude members as instructed by the user and get the resulting selection state immediately. For example, the following selection sequence:
QueryDimension dim;
dim.include(
Operator.DESCENDANTS,
IdentifierNode.ofNames("Time","2000").getSegmentList());
dim.exclude(
Operator.DESCENDANTS,
IdentifierNode.ofNames("Time","2000","Q1").getSegmentList());
dim.include(
Operator.MEMBER,
IdentifierNode.ofNames("Time","2000","Q1","April").getSegmentList());
will produce a different set of selected members in my implementation (April to December) than in the olap4j QueryDimension (May to December).
Usage Sample
This is the code used to initialize the query in the sample web app. It selects only the states of USA for the Store hierarchy, all the members of the Gender hierarchy and shows only Unit Sales and Measures.private Query initQuery() throws OlapException {
Cube c = getConnection().getOlapSchema().getCubes().get("Sales");
Query q = new Query("MyQuery", c);
QueryAxis columnsAxis = q.getAxis(Axis.COLUMNS);
columnsAxis.setNonEmpty(true);
columnsAxis.addDimension(selectAll(q, "Gender"));
QueryHierarchy measuresDim = q.getDimension("Measures");
columnsAxis.addDimension(measuresDim);
measuresDim.include(
Operator.MEMBER,
IdentifierNode.ofNames("Measures","Unit Sales").getSegmentList());
measuresDim.include(
Operator.MEMBER,
IdentifierNode.ofNames("Measures","Store Cost").getSegmentList());
QueryAxis rowsAxis = q.getAxis(Axis.ROWS);
rowsAxis.setNonEmpty(true);
QueryHierarchy storeDim = q.getDimension("Store");
storeDim.include(Operator.CHILDREN, IdentifierNode.ofNames("Store","USA").getSegmentList());
rowsAxis.addDimension(storeDim);
rowsAxis.addDimension(selectAll(q, "Store Type"));
return q;
}
private QueryHierarchy selectAll(Query q, String dimension) throws OlapException {
QueryHierarchy dim = q.getDimension(dimension);
dim.include(Operator.DESCENDANTS, dim.getHierarchy().getRootMembers().get(0));
return dim;
}
And this is the resulting output, after a pair of drills. Note that the drills controls at CA, OR and WA are a bug, they don't drill anything as those members have no children in this query hierarchy
Another design goal, suggested in a comment by Julian Hyde, was to define the selection mechanism in terms of hierarchies instead of dimensions, allowing selections on non-default hierarchies. So I renamed my QueryDimension to QueryHierarchy. My initial implementation supports only member selections using the operators MEMBER, CHILDREN, INCLUDE_CHILDREN and DESCENDANTS. The remaining member selections: ANCESTOR and SIBLING, can be implemented in terms of the previous ones and I decided to postpone implementation of level selections.
Implementation: Select as you Drill
The implementation of member selection is centered in the idea of including/excluding nodes at drilling time. Every usage of the hierarchy in a query axis is translated into a MDX expression with the following structureExclude(DrilldownMember(<include expression>,<drill expression>, RECURSIVE), <exclude expression>)
Those sets are generated with the following algorithm:
Initialize the <include expression> with the "roots" of the QueryHierarchy
for every drilled member M
add M to the <drill expression>
add to the <exclude expression> the excluded children of Madd to the <include expression> the "orphans" of M
The roots of the query hierarchy are those selected members having no selected ancestors in the query hierarchy. And the orphans of a member are those members, descendants of that member, having no selected ancestor below that member.
Another key point of the implementation is the way I store selection state for the members. It's stored as a tree of MemberSelectionState (an implementation class) keeping the operator includes and excludes issued for the member. And the children of the node are the children members that: override the selection dictated by its ancestor, or have any descendant overriding such a selection. This way of storing selections allows improvements to the previous algorithm that produce MDX expressions proportional in length to the number of drills executed on that usage of the hierarchy (refer to the QueryHierarchy.updateDrillSets() method implementation for details.
Query adaptation for handling hierarchies instead of dimensions
Using query hierarchies instead of query dimensions has an impact on the class Query. I've renamed the methods referring to the dimensions: getDimension to getHierarchy and getDimensions to getHierarchies. I've added the method getAvailableHierarchies to list the hierarchies that can be added to the current query. For a QueryHierarchy to be available nor It nor any hierarchy in the same dimension can be used in any axis.Next Steps
Augment QueryHierarchy with methods to expand levels, allowing presenting a pre-drilled hierarchy to the user, and implementing a method to test if a certain member is drillable in the query hierarchy (has selected descendants).Implement Level, ANCESTOR and SIBLING selections
Add filtering capabilities to Query
Implement a faces component to allow member selection.
Subscribe to:
Posts
(
Atom
)





