Showing posts with label JavaServer Faces. Show all posts
Showing posts with label JavaServer Faces. Show all posts

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
This version replaces the previous listbox and buttons used to add hierarchies to the query axes with a new composite component that:
  • 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.
All this functionality is added to the sample page with the following facelet snippet
<olap:queryAxisEditor
id="axisEditor"
query="#{queryBean}"
value="#{queryBean.selectedHierarchyName}">
<f:ajax
event="change"
execute=":form:axisEditor"
render=":form:axisEditor :form:hierarchyEditor"/>
<f:ajax
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
This component can fire two client side events:
  • 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.
This is the facelet used to implement this composite component
<composite:interface>
<composite:attribute
name="query"
/>
<composite:attribute
name="value"
/>
<composite:attribute
name="styleClass"
/>
<composite:attribute
name="headerClass"
/>
<composite:attribute
name="rowClasses"
/>
<composite:clientBehavior
name="change"
default="true"
event="click"
targets="columns rows filter unused"
/>
<composite:clientBehavior
name="edit"
event="edit"
targets="columns rows filter unused"
/>
<composite:clientBehavior
name="edit"
targets="swap"
/>
</composite:interface>
<composite:implementation>
<div
id="#{cc.attrs.clientId}">
<h:commandButton
id="swap"
styleClass="cgaofHierarchyListSwapBtn"
value="Swap Axes"
action="#{queryAxisEditorBean.swapAxes(cc.attrs.query)}"/>
<h:inputHidden
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"
styleClass="#{cc.attrs.styleClass}"
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"
styleClass="#{cc.attrs.styleClass}"
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"
rendered="#{cc.attrs.query.slicerHierarchies.size() > 0}"
styleClass="#{cc.attrs.styleClass}"
headerClass="#{cc.attrs.headerClass}"
rowClasses="#{cc.attrs.rowClasses}"/>
<olap:queryAxisEditorBase
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}"/>
</div>
</composite:implementation>

I'm using an inputHidden control to keep the selected hierarchy name: its value is changed by the inner queryAxisEditorBase controls, responsible for displaying the contents of a single axis. Those controls receive the query bean, the current selected hierarchy name, the client id of the hidden input field and the name of the axis they are displaying. Each axis is displayed using a table:
  • 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.
Both composite components (queryAxisEditor and queryAxisEditorBase) relay on the QueryAxisEditorBean managed bean to execute actions and simplify facelets.

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.


Screenshot of the sample application showing a filtered query and filter editor.
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):
  • 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.
I've also changed the <olap:queryHierarchyEditor> faces component, changing its appearance and behavior when editing a query hierarchy living in a slicer axis:
  • 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.
I've created a new faces composite component to show the contents of the slicer axis (active filters for the current query) and allow them to remove hierarchies from that axe. It' a table with a row for each hierarchy showing the hierarchy's caption in the first column and a descriptive text of the filter in the second column. This text lists the included members for simple query expressions and shows an informative label to indicate a filter too complex to be detailed.

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
This is the current implementation; with an immersion parameter to carry the current descendants include/exclude sign.
    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.
It generates a client event when the user removes a hierarchy from the filter axis. This code snippet is from the sample test page:
        <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:
  1. A contextual menu for each member.
    Probably the best solution but too complex, it will delay higher priority features (e.g.: filtering)
  2. Three buttons to toggle inclusion for each operator.The simplest solution to implement, but so many buttons would clutter the UI.
  3. 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".
This is a screenshot of the resulting solution:

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.
This abstract class, included in the olap4j-faces jar, implements writeObject and readObject to serialize the query state. The implementations of those members parallel those of the previous helper class, with an important difference: after serializing the query state, writeObject tears down the connection with the olap4j provider.
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

Sunday, December 11, 2011
You can download this version of the sample, and an improved version of the component library from here.
   


Screenshot of the version 1 sample webapp. Using an standard color scheme and
image buttons for drilling.
This entry describes how to leverage olap4j, JSF standard components and the drilling capability presented in my previous entry to create a drillable cellset table alla JPivot. First I explain how to add the necessary drill buttons, and then I describe the strategy I've chosen to save the query state between requests.


Adding Drill Buttons

Let's start modifying the contents in the <olap:cellSetAxis
forAxis="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.
So, the getQuery method is something like:
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

Wednesday, December 07, 2011
You can download the source code for the v0 component library and the sample web project here.

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.

I will use the delegated implementation rendering model for those components. So, following the pattern used in the JSF API, I’ll extend those component classes to handle HTML specific properties and methods. The resulting classes will be

·         HtmlCellSet, extending UICellSet

·         HtmlCellSetAxis, extending UICellSetAxis, and

·         HtmlCellSetCells, extending UICellSetCells

These classes will have an associated set of renderer classes: HtmlCellSetRenderer, HtmlCellSetCellsRenderer, HtmlColumnsAxisRenderer and HtmlRowsAxisRenderer. The last two renderers both render a HtmlCellSetAxis, but are specialized to render a columns (ordinal 0) axis and a rows (ordinal 1) axis. This is achived overriding the HtmlCellSetAxis.getRenderType() method to return a different type of renderer based in the type of CellSetAxis its bound to

       @Override

       public String getRendererType() {

             if (isFor(TableArea.columnAxis))

                    return "es.cgalesanco.faces.olap4j.columnsCellSetAxis";

             else

                    return "es.cgalesanco.faces.olap4j.rowsCellSetAxis";



       }



HtmlCellSetRenderer, drives the global rendering of a CellSet.

·         encodeBegin() renders the starting <table> element, the upper-left corner cell, and delegates (indirectly) on HtmlColumnsAxisRenderer the rendering of the <tr> tags for the rows containing the columns axis. In the last row of the columns axis, it delegates on HtmlRowsAxisRenderer to render the header cells for the rows axis.

·         encodeChildren() renders the cell set rows containing the rows axis and the data cells. Renders the <tr> elements and computes the first cell of the rows axis to be rendered in the row (taking into account previous cells rows spans); delegates the rendering of the cells on HtmlRowsAxisRenderer and HTmlCellSetCellsRenderer.

The following colored HTML shows which renderers renders which tag:

<table>

   <colgroup>

     <col/>

   </colgroup>

   <tr>

     <th>&nbsp;</th><th colspan=”2”>Measures</th>

   </tr>

   <tr>

     <th>Store</th><th>Unit Sales</th></th>Store Cost</th>

   </tr>

   <tr>

     <th>All Stores</th><td>266,733</td><td>225,627.23</td>

   </tr>

  <tr>

     <th>USA</th><td><td>266,733</td><td>225,627.23</td>

   </tr>

</table>




HtmlCellSetRenderer

HtmlRowsAxisRenderer

HtmlColumnsAxisRenderer

HtmlCellSetCellsRenderer



Styling the CellSet


This distribution of responsibilities rendering the table is used to style the table using the properties of the HTML components. The properties of these components are

HtmlCellSet

·         styleClass. The HTML style class passed through to the class attribute of the main <table> element.

·         cornerClass. The HTML style class passed through to the <th> element rendering the corner cell.

·         alternateClass. The HTML style class passed through to the <tr> elements starting the odd rows rendered by the HtmlCellSetRenderer (the gray rows in the previous colored HTML)

HtmlCellSetAxis

·         styleClass. The HTML style class passed through to the <tr> element rendered by the HtmlColumnsAxisRenderer or the <col> element rendered by the HtmlRowsAxisRenderer.

·         headerClass. The HTML style class passed through to the <th> elements containing the hierarchy headers.

Wrapping It Up


First, the @ManagedBean backing our sample CellSetTable; just change the getConnection() method to fit your olap4j provider and connection string. Caching the resulting CellSet is important, as the method getSampleCellSet() can be invoked repeatedly within the faces components.

@ManagedBean

public class OlapSample {

   private OlapConnection cn;

   private CellSet cs;

  

   public OlapConnection getConnection() throws SQLException {

      if ( cn != null )

          return cn;

     

      try {

      Class.forName("mondrian.olap4j.MondrianOlap4jDriver");

      } catch(ClassNotFoundException ex) {

          throw new RuntimeException(ex);

      }

      Connection jdbcCn = DriverManager.getConnection("jdbc:mondrian:"

             + "JdbcDrivers=com.mysql.jdbc.Driver;"

             + "Jdbc=jdbc:mysql://localhost/foodmart;"

             + "JdbcUser=root;JdbcPassword=root;"

             + "Catalog=file:/users/cesar/FoodMart.xml");

      return cn = jdbcCn.unwrap(OlapConnection.class);

   }

  

   @PreDestroy

   public void tearDown() throws SQLException {

      if ( cn != null )

          cn.close();

   }

  

   public CellSet getSampleCellSet() throws ClassNotFoundException, SQLException {

      if ( cs != null )

          return cs;

     

      OlapConnection cn = getConnection();

     

      return cs = cn.createStatement().executeOlapQuery(

             "SELECT " +

             "    CrossJoin([Gender].AllMembers,[Measures].AllMembers) ON COLUMNS," +

             "    NON EMPTY CrossJoin([Store].AllMembers,[Store Type].AllMembers) ON ROWS " +

             "FROM Sales");

   }

}



And the Facelets XHTML page

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"

          "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml"

   xmlns:ui="http://java.sun.com/jsf/facelets"

   xmlns:h="http://java.sun.com/jsf/html"

   xmlns:f="http://java.sun.com/jsf/core"

   xmlns:olap="http://cgalesanco.es/faces/olap4j">

<h:head>

   <title>First CellSetTable</title>

  

   <style type="text/css">

   … see below …

   </style>

</h:head>

<h:body>

   <olap:cellSetTable value="#{olapSample.sampleCellSet}"

     styleClass="cellSet"

     cornerClass="cellSetCorner"

     alternateClass="alternate">

    

     <olap:cellSetAxis forAxis="columns" var="m"

        styleClass="columnsAxisClass"

        headerClass="columnsAxisHeaderClass">

        <f:facet name="header">

           <h:outputText value="#{m.hierarchy.caption}" />

        </f:facet>

        <h:outputText value="#{m.member.caption}"/>

     </olap:cellSetAxis>

    

     <olap:cellSetAxis forAxis="rows" var="m"

        styleClass="rowsAxisClass"

        headerClass="rowsAxisHeaderClass">

        <f:facet name="header">

           <h:outputText value="#{m.hierarchy.caption}"/>

        </f:facet>

        <!-- Indents the member name based on its depth -->

        <h:outputText value="#{m.member.caption}" style="padding-left:#{m.member.depth}ex"/>

     </olap:cellSetAxis>

    

     <olap:cellSetCells var="cell">

        <h:outputText value="#{cell.cell.formattedValue}"/>              

     </olap:cellSetCells>

    

   </olap:cellSetTable>

</h:body>

</html>



It has the structure discussed in my previous post; I’ve added the class attributes to allow styling and you can see how I’ve implemented member indentation in the rows axis using the Member.getDepth()method.

And this is the CSS styles I’ve used to render the table. They pretend to be as pedagogical as possible, so I beg your pardon about the color scheme.

     body { font-family:Verdana,Helvetica; font-size:small }

  

     /* Every header cell will be aligned on the left and top */

     .cellSet th { text-align:left;white-space:nowrap;vertical-align:top; }

    

     /* Every data cell will be aligned on the right */

     .cellSet td { text-align:right;white-space:nowrap }

    

     /* Sets background color for rowsAxis cells */

     .cellSet col.rowsAxisClass { background-color:red }

    

     /* Sets background color for columnsAxis cells */

     .cellSet tr.columnsAxisClass { background-color:blue;color:white; }

    

     /* Overrides colors for hierarchy header cells within the columnsAxis */

     .cellSet th.columnsAxisHeaderClass { background-color:darkblue; color:white; }

    

     /* Overrides colors for hierarchy header cells within the rowsAxis */

     .cellSet th.rowsAxisHeaderClass { background-color:maroon; color:white }

    

     /* Sets style for the upper-left corner empty cell */

     .cellSet th.cellSetCorner { background-color:yellow; }

    

     /* Sets style for alternating rowsAxis background color */

     .cellSet tr.alternate th {background-color:darksalmon}

    

     /* Sets style for alternating data cells background color */

     .cellSet tr.alternate td {background-color:gainsboro}