View Javadoc

1   /*
2    * ====================================================================
3    * This software is subject to the terms of the Common Public License
4    * Agreement, available at the following URL:
5    *   http://www.opensource.org/licenses/cpl.html .
6    * You must accept the terms of that agreement to use this software.
7    * ====================================================================
8    */
9   package com.eyeq.pivot4j.ui.table;
10  
11  import static com.eyeq.pivot4j.ui.CellTypes.AGG_VALUE;
12  import static com.eyeq.pivot4j.ui.CellTypes.LABEL;
13  import static com.eyeq.pivot4j.ui.CellTypes.VALUE;
14  import static com.eyeq.pivot4j.ui.table.TableCellTypes.FILL;
15  import static com.eyeq.pivot4j.ui.table.TableCellTypes.TITLE;
16  import static com.eyeq.pivot4j.ui.table.TablePropertyCategories.CELL;
17  import static com.eyeq.pivot4j.ui.table.TablePropertyCategories.HEADER;
18  
19  import java.io.Serializable;
20  import java.util.ArrayList;
21  import java.util.Collections;
22  import java.util.Comparator;
23  import java.util.HashMap;
24  import java.util.LinkedHashSet;
25  import java.util.LinkedList;
26  import java.util.List;
27  import java.util.Map;
28  import java.util.Set;
29  
30  import org.apache.commons.configuration.HierarchicalConfiguration;
31  import org.apache.commons.lang.NullArgumentException;
32  import org.apache.commons.lang.StringUtils;
33  import org.olap4j.Axis;
34  import org.olap4j.Cell;
35  import org.olap4j.CellSet;
36  import org.olap4j.CellSetAxis;
37  import org.olap4j.OlapException;
38  import org.olap4j.Position;
39  import org.olap4j.metadata.Hierarchy;
40  import org.olap4j.metadata.Level;
41  import org.olap4j.metadata.Measure;
42  import org.olap4j.metadata.Member;
43  import org.olap4j.metadata.Property;
44  import org.slf4j.Logger;
45  import org.slf4j.LoggerFactory;
46  
47  import com.eyeq.pivot4j.PivotException;
48  import com.eyeq.pivot4j.PivotModel;
49  import com.eyeq.pivot4j.transform.ChangeSlicer;
50  import com.eyeq.pivot4j.ui.AbstractPivotRenderer;
51  import com.eyeq.pivot4j.ui.aggregator.Aggregator;
52  import com.eyeq.pivot4j.ui.aggregator.AggregatorFactory;
53  import com.eyeq.pivot4j.ui.aggregator.AggregatorPosition;
54  import com.eyeq.pivot4j.util.OlapUtils;
55  import com.eyeq.pivot4j.util.RaggedMemberWrapper;
56  import com.eyeq.pivot4j.util.TreeNode;
57  import com.eyeq.pivot4j.util.TreeNodeCallback;
58  
59  public class TableRenderer extends
60  		AbstractPivotRenderer<TableRenderContext, TableRenderCallback> {
61  
62  	private Logger logger = LoggerFactory.getLogger(getClass());
63  
64  	private boolean hideSpans = false;
65  
66  	private boolean showParentMembers = false;
67  
68  	private boolean showDimensionTitle = true;
69  
70  	/**
71  	 * @see com.eyeq.pivot4j.ui.AbstractPivotRenderer#getRenderPropertyCategories()
72  	 */
73  	@Override
74  	protected List<String> getRenderPropertyCategories() {
75  		List<String> categories = new LinkedList<String>(
76  				super.getRenderPropertyCategories());
77  
78  		categories.add(CELL);
79  		categories.add(HEADER);
80  
81  		return categories;
82  	}
83  
84  	/**
85  	 * @return the hideSpans
86  	 */
87  	public boolean getHideSpans() {
88  		return hideSpans;
89  	}
90  
91  	/**
92  	 * @param hideSpans
93  	 *            the hideSpans to set
94  	 */
95  	public void setHideSpans(boolean hideSpans) {
96  		this.hideSpans = hideSpans;
97  	}
98  
99  	/**
100 	 * @return the showParentMembers
101 	 */
102 	public boolean getShowParentMembers() {
103 		return showParentMembers;
104 	}
105 
106 	/**
107 	 * @param showParentMembers
108 	 *            the showParentMembers to set
109 	 */
110 	public void setShowParentMembers(boolean showParentMembers) {
111 		this.showParentMembers = showParentMembers;
112 	}
113 
114 	/**
115 	 * @return the showDimensionTitle
116 	 */
117 	public boolean getShowDimensionTitle() {
118 		return showDimensionTitle;
119 	}
120 
121 	/**
122 	 * @param showDimensionTitle
123 	 *            the showDimensionTitle to set
124 	 */
125 	public void setShowDimensionTitle(boolean showDimensionTitle) {
126 		this.showDimensionTitle = showDimensionTitle;
127 	}
128 
129 	public void swapAxes() {
130 		for (AggregatorPosition position : AggregatorPosition.values()) {
131 			swapAggregators(position);
132 		}
133 	}
134 
135 	/**
136 	 * @param position
137 	 */
138 	private void swapAggregators(AggregatorPosition position) {
139 		List<String> aggregators = getAggregators(Axis.COLUMNS, position);
140 
141 		setAggregators(Axis.COLUMNS, position,
142 				getAggregators(Axis.ROWS, position));
143 		setAggregators(Axis.ROWS, position, aggregators);
144 	}
145 
146 	/**
147 	 * @param context
148 	 * @return
149 	 * @see com.eyeq.pivot4j.ui.AbstractPivotRenderer#getLabel(com.eyeq.pivot4j.ui.RenderContext)
150 	 */
151 	@Override
152 	protected String getLabel(TableRenderContext context) {
153 		String label;
154 
155 		if (LABEL.equals(context.getCellType())) {
156 			label = getHeaderLabel(context);
157 		} else if (TITLE.equals(context.getCellType())) {
158 			label = getTitleLabel(context);
159 		} else if (VALUE.equals(context.getCellType())) {
160 			label = getValueLabel(context);
161 		} else if (AGG_VALUE.equals(context.getCellType())) {
162 			label = getAggregationLabel(context);
163 		} else {
164 			label = null;
165 		}
166 
167 		return label;
168 	}
169 
170 	/**
171 	 * @param context
172 	 * @return
173 	 */
174 	protected String getHeaderLabel(TableRenderContext context) {
175 		String label;
176 
177 		if (context.getProperty() == null) {
178 			if (context.getMember() == null) {
179 				label = context.getHierarchy().getCaption();
180 			} else {
181 				label = context.getMember().getCaption();
182 			}
183 		} else {
184 			try {
185 				label = context.getMember().getPropertyFormattedValue(
186 						context.getProperty());
187 			} catch (OlapException e) {
188 				throw new PivotException(e);
189 			}
190 		}
191 
192 		return label;
193 	}
194 
195 	/**
196 	 * @param context
197 	 * @return
198 	 */
199 	protected String getTitleLabel(TableRenderContext context) {
200 		String label = null;
201 
202 		if (context.getAxis() == Axis.FILTER) {
203 			label = context.getResourceBundle().getString("label.filter");
204 		} else if (context.getProperty() != null) {
205 			label = context.getProperty().getCaption();
206 		} else if (context.getLevel() != null) {
207 			label = context.getLevel().getCaption();
208 		} else if (context.getHierarchy() != null) {
209 			label = context.getHierarchy().getCaption();
210 		}
211 
212 		return label;
213 	}
214 
215 	/**
216 	 * @param context
217 	 * @return
218 	 */
219 	protected String getValueLabel(TableRenderContext context) {
220 		String label;
221 
222 		Cell cell = context.getCell();
223 
224 		if (cell == null) {
225 			if (context.getAxis() == Axis.FILTER) {
226 				if (context.getMember() != null) {
227 					label = context.getMember().getCaption();
228 				} else {
229 					label = context.getHierarchy().getCaption();
230 				}
231 			} else {
232 				Aggregator aggregator = context.getAggregator();
233 
234 				if (aggregator == null) {
235 					label = null;
236 				} else {
237 					label = aggregator.getFormattedValue(context);
238 				}
239 			}
240 		} else {
241 			label = cell.getFormattedValue();
242 		}
243 
244 		return label;
245 	}
246 
247 	/**
248 	 * @param context
249 	 * @return
250 	 */
251 	protected String getAggregationLabel(TableRenderContext context) {
252 		String label;
253 
254 		Aggregator aggregator = context.getAggregator();
255 
256 		if (aggregator == null && context.getMember() != null) {
257 			label = context.getMember().getCaption();
258 		} else {
259 			label = aggregator.getLabel(context);
260 		}
261 
262 		return label;
263 	}
264 
265 	/**
266 	 * @param context
267 	 * @return
268 	 */
269 	@Override
270 	protected String getRenderPropertyCategory(TableRenderContext context) {
271 		String category;
272 
273 		if (VALUE.equals(context.getCellType())
274 				|| AGG_VALUE.equals(context.getCellType())) {
275 			category = CELL;
276 		} else {
277 			category = HEADER;
278 		}
279 
280 		return category;
281 	}
282 
283 	/**
284 	 * @see com.eyeq.pivot4j.ui.AbstractPivotRenderer#saveState()
285 	 */
286 	@Override
287 	public Serializable saveState() {
288 		Serializable[] states = new Serializable[6];
289 
290 		int index = 0;
291 
292 		states[index++] = super.saveState();
293 		states[index++] = showParentMembers;
294 		states[index++] = showDimensionTitle;
295 		states[index++] = hideSpans;
296 
297 		return states;
298 	}
299 
300 	/**
301 	 * @see com.eyeq.pivot4j.ui.AbstractPivotRenderer#restoreState(java.io.Serializable)
302 	 */
303 	@Override
304 	public void restoreState(Serializable state) {
305 		if (state == null) {
306 			throw new NullArgumentException("state");
307 		}
308 
309 		Serializable[] states = (Serializable[]) state;
310 
311 		int index = 0;
312 
313 		super.restoreState(states[index++]);
314 
315 		this.showParentMembers = (Boolean) states[index++];
316 		this.showDimensionTitle = (Boolean) states[index++];
317 		this.hideSpans = (Boolean) states[index++];
318 	}
319 
320 	/**
321 	 * @see com.eyeq.pivot4j.ui.AbstractPivotRenderer#saveSettings(org.apache.commons.configuration.HierarchicalConfiguration)
322 	 */
323 	@Override
324 	public void saveSettings(HierarchicalConfiguration configuration) {
325 		super.saveSettings(configuration);
326 
327 		configuration.addProperty("showDimensionTitle", showDimensionTitle);
328 		configuration.addProperty("showParentMembers", showParentMembers);
329 		configuration.addProperty("hideSpans", hideSpans);
330 	}
331 
332 	/**
333 	 * @see com.eyeq.pivot4j.ui.AbstractPivotRenderer#restoreSettings(org.apache.commons.configuration.HierarchicalConfiguration)
334 	 */
335 	@Override
336 	public void restoreSettings(HierarchicalConfiguration configuration) {
337 		super.restoreSettings(configuration);
338 
339 		this.showDimensionTitle = configuration.getBoolean(
340 				"showDimensionTitle", true);
341 		this.showParentMembers = configuration.getBoolean("showParentMembers",
342 				false);
343 		this.hideSpans = configuration.getBoolean("hideSpans", false);
344 	}
345 
346 	/**
347 	 * @see com.eyeq.pivot4j.ui.AbstractPivotRenderer#render(com.eyeq.pivot4j.ui.RenderContext,
348 	 *      com.eyeq.pivot4j.ui.RenderCallback)
349 	 */
350 	@Override
351 	public void render(PivotModel model, TableRenderCallback callback) {
352 		if (model == null) {
353 			throw new NullArgumentException("model");
354 		}
355 
356 		if (callback == null) {
357 			throw new NullArgumentException("callback");
358 		}
359 
360 		CellSet cellSet = model.getCellSet();
361 
362 		if (cellSet == null) {
363 			return;
364 		}
365 
366 		List<CellSetAxis> axes = cellSet.getAxes();
367 		if (axes.isEmpty()) {
368 			return;
369 		}
370 
371 		TableHeaderNode columnRoot = createAxisTree(model, Axis.COLUMNS);
372 		if (columnRoot == null) {
373 			return;
374 		}
375 
376 		TableHeaderNode rowRoot = createAxisTree(model, Axis.ROWS);
377 		if (rowRoot == null) {
378 			return;
379 		}
380 
381 		configureAxisTree(model, Axis.COLUMNS, columnRoot);
382 		configureAxisTree(model, Axis.ROWS, rowRoot);
383 
384 		invalidateAxisTree(model, Axis.COLUMNS, columnRoot);
385 		invalidateAxisTree(model, Axis.ROWS, rowRoot);
386 
387 		TableRenderContext context = createRenderContext(model, columnRoot,
388 				rowRoot);
389 
390 		callback.startRender(context);
391 		callback.startTable(context);
392 
393 		renderHeader(context, columnRoot, rowRoot, callback);
394 		renderBody(context, columnRoot, rowRoot, callback);
395 
396 		callback.endTable(context);
397 
398 		if (getRenderSlicer()) {
399 			renderFilter(context, callback);
400 		}
401 
402 		callback.endRender(context);
403 	}
404 
405 	/**
406 	 * @param model
407 	 * @param columnRoot
408 	 * @param rowRoot
409 	 * @return
410 	 */
411 	protected TableRenderContext createRenderContext(PivotModel model,
412 			TableHeaderNode columnRoot, TableHeaderNode rowRoot) {
413 		int columnHeaderCount = columnRoot.getMaxRowIndex();
414 		int rowHeaderCount = rowRoot.getMaxRowIndex();
415 
416 		int columnCount = columnRoot.getWidth();
417 		int rowCount = rowRoot.getWidth();
418 
419 		Map<String, Member> cachedParents = new HashMap<String, Member>();
420 
421 		cachedParents.putAll(columnRoot.getReference().getParentMembersCache());
422 		cachedParents.putAll(rowRoot.getReference().getParentMembersCache());
423 
424 		TableRenderContext context = new TableRenderContext(model, this,
425 				columnCount, rowCount, columnHeaderCount, rowHeaderCount);
426 
427 		context.setAttribute(TableRenderContext.ATTRIBUTE_CACHED_MEMBERS,
428 				cachedParents);
429 
430 		return context;
431 	}
432 
433 	/**
434 	 * @param context
435 	 * @param columnRoot
436 	 * @param rowRoot
437 	 * @param callback
438 	 */
439 	protected void renderHeader(final TableRenderContext context,
440 			final TableHeaderNode columnRoot, final TableHeaderNode rowRoot,
441 			final TableRenderCallback callback) {
442 
443 		callback.startHeader(context);
444 		context.setRenderPropertyCategory(HEADER);
445 
446 		int count = context.getColumnHeaderCount();
447 
448 		for (int rowIndex = 0; rowIndex < count; rowIndex++) {
449 			context.setAxis(Axis.COLUMNS);
450 			context.setColIndex(0);
451 			context.setRowIndex(rowIndex);
452 
453 			callback.startRow(context);
454 
455 			renderHeaderCorner(context, columnRoot, rowRoot, callback);
456 
457 			// invoking renderHeaderCorner method resets the axis property.
458 			context.setAxis(Axis.COLUMNS);
459 
460 			columnRoot.walkChildrenAtRowIndex(
461 					new TreeNodeCallback<TableAxisContext>() {
462 
463 						@Override
464 						public int handleTreeNode(
465 								TreeNode<TableAxisContext> node) {
466 							TableHeaderNode headerNode = (TableHeaderNode) node;
467 
468 							context.setColIndex(headerNode.getColIndex()
469 									+ context.getRowHeaderCount());
470 							context.setColSpan(headerNode.getColSpan());
471 							context.setRowSpan(headerNode.getRowSpan());
472 
473 							context.setMember(headerNode.getMember());
474 							context.setProperty(headerNode.getProperty());
475 							context.setHierarchy(headerNode.getHierarchy());
476 							context.setPosition(headerNode.getPosition());
477 							context.setColumnPosition(headerNode.getPosition());
478 							context.setAggregator(headerNode.getAggregator());
479 							context.setCell(null);
480 
481 							if (headerNode.isAggregation()) {
482 								context.setCellType(AGG_VALUE);
483 							} else if (context.getMember() == null) {
484 								if (context.getHierarchy() == null) {
485 									context.setCellType(FILL);
486 								} else {
487 									context.setCellType(TITLE);
488 								}
489 							} else {
490 								context.setCellType(LABEL);
491 							}
492 
493 							callback.startCell(context);
494 							callback.renderCommands(context,
495 									getCommands(context));
496 							callback.renderContent(context, getLabel(context));
497 							callback.endCell(context);
498 
499 							return TreeNodeCallback.CONTINUE;
500 						}
501 					}, rowIndex + 1);
502 
503 			callback.endRow(context);
504 		}
505 
506 		callback.endHeader(context);
507 	}
508 
509 	/**
510 	 * @param context
511 	 * @param rowRoot
512 	 * @param columnRoot
513 	 * @param callback
514 	 */
515 	protected void renderBody(final TableRenderContext context,
516 			final TableHeaderNode columnRoot, final TableHeaderNode rowRoot,
517 			final TableRenderCallback callback) {
518 		callback.startBody(context);
519 
520 		int count = rowRoot.getColSpan();
521 
522 		for (int rowIndex = 0; rowIndex < count; rowIndex++) {
523 			context.setAxis(Axis.ROWS);
524 			context.setColIndex(0);
525 			context.setRowIndex(rowIndex + context.getColumnHeaderCount());
526 
527 			callback.startRow(context);
528 
529 			rowRoot.walkChildrenAtColIndex(
530 					new TreeNodeCallback<TableAxisContext>() {
531 
532 						@Override
533 						public int handleTreeNode(
534 								TreeNode<TableAxisContext> node) {
535 							TableHeaderNode headerNode = (TableHeaderNode) node;
536 
537 							if (headerNode.getRowIndex() == 0) {
538 								return TreeNodeCallback.CONTINUE;
539 							}
540 
541 							context.setColIndex(headerNode.getRowIndex() - 1);
542 							context.setColSpan(headerNode.getRowSpan());
543 							context.setRowSpan(headerNode.getColSpan());
544 							context.setMember(headerNode.getMember());
545 							context.setLevel(headerNode.getMemberLevel());
546 							context.setProperty(headerNode.getProperty());
547 							context.setHierarchy(headerNode.getHierarchy());
548 							context.setPosition(headerNode.getPosition());
549 							context.setRowPosition(headerNode.getPosition());
550 							context.setAggregator(headerNode.getAggregator());
551 							context.setCell(null);
552 							context.setRenderPropertyCategory(HEADER);
553 
554 							if (headerNode.isAggregation()) {
555 								context.setCellType(AGG_VALUE);
556 							} else if (context.getMember() == null) {
557 								if (context.getHierarchy() == null) {
558 									context.setCellType(FILL);
559 								} else {
560 									context.setCellType(TITLE);
561 								}
562 							} else {
563 								context.setCellType(LABEL);
564 							}
565 
566 							callback.startCell(context);
567 							callback.renderCommands(context,
568 									getCommands(context));
569 							callback.renderContent(context, getLabel(context));
570 							callback.endCell(context);
571 
572 							if (headerNode.getChildCount() == 0) {
573 								renderDataRow(context, columnRoot, rowRoot,
574 										(TableHeaderNode) node, callback);
575 							}
576 
577 							return TreeNodeCallback.CONTINUE;
578 						}
579 					}, rowIndex);
580 
581 			callback.endRow(context);
582 		}
583 
584 		callback.endBody(context);
585 	}
586 
587 	/**
588 	 * @param context
589 	 * @param columnRoot
590 	 * @param rowRoot
591 	 * @param rowNode
592 	 * @param callback
593 	 */
594 	protected void renderDataRow(TableRenderContext context,
595 			TableHeaderNode columnRoot, TableHeaderNode rowRoot,
596 			TableHeaderNode rowNode, TableRenderCallback callback) {
597 		context.setCellType(VALUE);
598 		context.setRenderPropertyCategory(CELL);
599 
600 		for (int i = 0; i < context.getColumnCount(); i++) {
601 			Cell cell = null;
602 
603 			TableHeaderNode columnNode = columnRoot.getLeafNodeAtColIndex(i);
604 
605 			if (columnNode != null && columnNode.getPosition() != null
606 					&& columnNode.getPosition().getOrdinal() != -1
607 					&& rowNode.getPosition() != null
608 					&& rowNode.getPosition().getOrdinal() != -1) {
609 				cell = context.getCellSet().getCell(columnNode.getPosition(),
610 						rowNode.getPosition());
611 			}
612 
613 			context.setColIndex(context.getRowHeaderCount() + i);
614 			context.setColSpan(1);
615 			context.setRowSpan(1);
616 			context.setAggregator(null);
617 
618 			context.setAxis(null);
619 			context.setHierarchy(null);
620 			context.setLevel(null);
621 			context.setMember(null);
622 			context.setCell(cell);
623 
624 			context.setPosition(null);
625 			context.setColumnPosition(columnNode.getPosition());
626 			context.setRowPosition(rowNode.getPosition());
627 
628 			if (columnNode.getAggregator() == null) {
629 				if (rowNode.getAggregator() != null) {
630 					context.setAggregator(rowNode.getAggregator());
631 					context.setAxis(Axis.ROWS);
632 				}
633 			} else if (rowNode.getAggregator() == null
634 					|| columnNode.getAggregator().getMeasure() != null) {
635 				context.setAggregator(columnNode.getAggregator());
636 				context.setAxis(Axis.COLUMNS);
637 			} else if (rowNode.getAggregator().getMeasure() != null) {
638 				context.setAggregator(rowNode.getAggregator());
639 				context.setAxis(Axis.ROWS);
640 			}
641 
642 			callback.startCell(context);
643 			callback.renderCommands(context, getCommands(context));
644 			callback.renderContent(context, getLabel(context));
645 			callback.endCell(context);
646 
647 			context.setPosition(context.getRowPosition());
648 
649 			for (AggregatorPosition position : AggregatorPosition.values()) {
650 				for (Aggregator aggregator : rowRoot.getReference()
651 						.getAggregators(position)) {
652 					aggregate(context, rowNode, aggregator, position);
653 				}
654 			}
655 
656 			context.setPosition(context.getColumnPosition());
657 
658 			for (AggregatorPosition position : AggregatorPosition.values()) {
659 				for (Aggregator aggregator : columnRoot.getReference()
660 						.getAggregators(position)) {
661 					aggregate(context, columnNode, aggregator, position);
662 				}
663 			}
664 		}
665 
666 		context.setAggregator(null);
667 	}
668 
669 	/**
670 	 * @param context
671 	 * @param node
672 	 * @param aggregator
673 	 * @param position
674 	 */
675 	protected void aggregate(TableRenderContext context, TableHeaderNode node,
676 			Aggregator aggregator, AggregatorPosition position) {
677 		Measure measure = aggregator.getMeasure();
678 
679 		List<Member> members = aggregator.getMembers();
680 
681 		if (context.getCell() == null
682 				&& (measure == null || context.getAggregator() == null)) {
683 			return;
684 		}
685 
686 		if (context.getAggregator() != null
687 				&& (context.getAggregator().getAxis() == aggregator.getAxis())) {
688 			return;
689 		}
690 
691 		List<Member> positionMembers = context.getPosition().getMembers();
692 
693 		int index = 0;
694 		for (Member member : members) {
695 			if (positionMembers.size() <= index) {
696 				return;
697 			}
698 
699 			Member positionMember = positionMembers.get(index);
700 
701 			if (positionMember.getDepth() > 1
702 					&& context.getParentMember(positionMember) == null) {
703 				positionMember = new RaggedMemberWrapper(positionMember,
704 						context.getModel().getCube());
705 			}
706 
707 			if (!OlapUtils.equals(member, positionMember)
708 					&& (member.getDepth() >= positionMember.getDepth() || !context
709 							.getAncestorMembers(positionMember)
710 							.contains(member))) {
711 				return;
712 			}
713 
714 			index++;
715 		}
716 
717 		if (measure != null && !positionMembers.isEmpty()) {
718 			Member member = positionMembers.get(positionMembers.size() - 1);
719 
720 			if (!measure.equals(member)) {
721 				return;
722 			}
723 		}
724 
725 		TableHeaderNode parent = node;
726 
727 		while (parent != null) {
728 			if (parent.getHierarchyDescendents() == 1
729 					&& parent.getMemberChildren() > 0) {
730 				switch (position) {
731 				case Grand:
732 					return;
733 				case Hierarchy:
734 					if (!members.contains(parent.getMember())) {
735 						return;
736 					}
737 					break;
738 				case Member:
739 					if (node == parent
740 							|| members.lastIndexOf(parent.getMember()) == members
741 									.size() - 1) {
742 						return;
743 					}
744 					break;
745 				default:
746 					assert false;
747 				}
748 			}
749 
750 			parent = (TableHeaderNode) parent.getParent();
751 		}
752 
753 		aggregator.aggregate(context);
754 	}
755 
756 	/**
757 	 * @param context
758 	 * @param columnRoot
759 	 * @param rowRoot
760 	 * @param callback
761 	 */
762 	protected void renderHeaderCorner(TableRenderContext context,
763 			TableHeaderNode columnRoot, TableHeaderNode rowRoot,
764 			TableRenderCallback callback) {
765 		int offset = 0;
766 
767 		if (getShowDimensionTitle()) {
768 			offset = showParentMembers ? 2 : 1;
769 		}
770 
771 		context.setAxis(null);
772 
773 		context.setHierarchy(null);
774 		context.setLevel(null);
775 		context.setMember(null);
776 		context.setProperty(null);
777 
778 		context.setCell(null);
779 		context.setCellType(FILL);
780 
781 		context.setPosition(null);
782 		context.setColumnPosition(null);
783 		context.setRowPosition(null);
784 
785 		boolean renderDimensionTitle = showDimensionTitle
786 				&& (context.getRowIndex() == context.getColumnHeaderCount()
787 						- offset);
788 		boolean renderLevelTitle = showDimensionTitle
789 				&& showParentMembers
790 				&& (context.getRowIndex() == context.getColumnHeaderCount() - 1);
791 
792 		if (context.getRowIndex() == 0 && !renderDimensionTitle
793 				&& !renderLevelTitle) {
794 			context.setColSpan(context.getRowHeaderCount());
795 			context.setRowSpan(context.getColumnHeaderCount() - offset);
796 
797 			callback.startCell(context);
798 			callback.renderCommands(context, getCommands(context));
799 			callback.renderContent(context, getLabel(context));
800 			callback.endCell(context);
801 		} else if (renderDimensionTitle) {
802 			final Map<Hierarchy, Integer> spans = new HashMap<Hierarchy, Integer>();
803 			final Map<Hierarchy, List<Property>> propertyMap = new HashMap<Hierarchy, List<Property>>();
804 
805 			rowRoot.walkChildrenAtColIndex(
806 					new TreeNodeCallback<TableAxisContext>() {
807 
808 						@Override
809 						public int handleTreeNode(
810 								TreeNode<TableAxisContext> node) {
811 							TableHeaderNode headerNode = (TableHeaderNode) node;
812 							if (headerNode.getHierarchy() == null) {
813 								return TreeNodeCallback.CONTINUE;
814 							}
815 
816 							Hierarchy hierarchy = headerNode.getHierarchy();
817 
818 							if (headerNode.getProperty() == null) {
819 								Integer span = spans.get(hierarchy);
820 								if (span == null) {
821 									span = 0;
822 								}
823 
824 								span += headerNode.getRowSpan();
825 								spans.put(headerNode.getHierarchy(), span);
826 							} else {
827 								List<Property> properties = propertyMap
828 										.get(hierarchy);
829 								if (properties == null) {
830 									properties = new ArrayList<Property>();
831 									propertyMap.put(hierarchy, properties);
832 								}
833 
834 								properties.add(headerNode.getProperty());
835 							}
836 
837 							return TreeNodeCallback.CONTINUE;
838 						}
839 					}, 0);
840 
841 			context.setAxis(Axis.ROWS);
842 			context.setRowSpan(1);
843 			context.setCellType(TITLE);
844 
845 			for (Hierarchy hierarchy : rowRoot.getReference().getHierarchies()) {
846 				Integer span = spans.get(hierarchy);
847 				if (span == null) {
848 					span = 1;
849 				}
850 
851 				context.setColSpan(span);
852 				context.setHierarchy(hierarchy);
853 
854 				callback.startCell(context);
855 				callback.renderCommands(context, getCommands(context));
856 				callback.renderContent(context, getLabel(context));
857 				callback.endCell(context);
858 
859 				context.setColIndex(context.getColumnIndex() + span);
860 
861 				List<Property> properties = propertyMap.get(hierarchy);
862 				if (properties != null) {
863 					for (Property property : properties) {
864 						context.setColSpan(1);
865 						context.setColIndex(context.getColumnIndex() + 1);
866 						context.setProperty(property);
867 
868 						callback.startCell(context);
869 						callback.renderCommands(context, getCommands(context));
870 						callback.renderContent(context, getLabel(context));
871 						callback.endCell(context);
872 					}
873 				}
874 			}
875 		} else if (renderLevelTitle) {
876 			final Map<Integer, Level> levels = new HashMap<Integer, Level>();
877 			final Map<Integer, Property> properties = new HashMap<Integer, Property>();
878 
879 			rowRoot.walkChildren(new TreeNodeCallback<TableAxisContext>() {
880 
881 				@Override
882 				public int handleTreeNode(TreeNode<TableAxisContext> node) {
883 					TableHeaderNode headerNode = (TableHeaderNode) node;
884 					int colIndex = headerNode.getRowIndex() - 1;
885 
886 					if (headerNode.getMember() != null
887 							&& !levels.containsKey(colIndex)) {
888 						levels.put(colIndex, headerNode.getMember().getLevel());
889 					}
890 
891 					if (headerNode.getProperty() != null
892 							&& !properties.containsKey(colIndex)) {
893 						properties.put(colIndex, headerNode.getProperty());
894 					}
895 
896 					return TreeNodeCallback.CONTINUE;
897 				}
898 			});
899 
900 			context.setAxis(Axis.ROWS);
901 			context.setColSpan(1);
902 			context.setRowSpan(1);
903 			context.setCellType(TITLE);
904 
905 			for (int i = 0; i < context.getRowHeaderCount(); i++) {
906 				context.setColIndex(i);
907 
908 				Level level = levels.get(i);
909 
910 				if (level == null) {
911 					context.setHierarchy(null);
912 					context.setLevel(null);
913 				} else {
914 					context.setHierarchy(level.getHierarchy());
915 					context.setLevel(level);
916 				}
917 
918 				context.setProperty(properties.get(i));
919 
920 				callback.startCell(context);
921 				callback.renderCommands(context, getCommands(context));
922 				callback.renderContent(context, getLabel(context));
923 				callback.endCell(context);
924 			}
925 		}
926 
927 		context.setHierarchy(null);
928 	}
929 
930 	/**
931 	 * @param model
932 	 * @param axis
933 	 * @return
934 	 */
935 	protected TableHeaderNode createAxisTree(PivotModel model, Axis axis) {
936 		List<CellSetAxis> axes = model.getCellSet().getAxes();
937 
938 		if (axes.size() < 2) {
939 			return null;
940 		}
941 
942 		CellSetAxis cellSetAxis = axes.get(axis.axisOrdinal());
943 
944 		List<Position> positions = cellSetAxis.getPositions();
945 		if (positions == null || positions.isEmpty()) {
946 			return null;
947 		}
948 
949 		List<Hierarchy> hierarchies = new ArrayList<Hierarchy>();
950 
951 		List<Aggregator> aggregators = new ArrayList<Aggregator>();
952 		List<Aggregator> hierarchyAggregators = new ArrayList<Aggregator>();
953 		List<Aggregator> memberAggregators = new ArrayList<Aggregator>();
954 
955 		Map<AggregatorPosition, List<Aggregator>> aggregatorMap = new HashMap<AggregatorPosition, List<Aggregator>>();
956 		aggregatorMap.put(AggregatorPosition.Grand, aggregators);
957 		aggregatorMap.put(AggregatorPosition.Hierarchy, hierarchyAggregators);
958 		aggregatorMap.put(AggregatorPosition.Member, memberAggregators);
959 
960 		Map<Hierarchy, List<Level>> levelsMap = new HashMap<Hierarchy, List<Level>>();
961 
962 		Comparator<Level> levelComparator = new Comparator<Level>() {
963 
964 			@Override
965 			public int compare(Level l1, Level l2) {
966 				Integer d1 = l1.getDepth();
967 				Integer d2 = l2.getDepth();
968 				return d1.compareTo(d2);
969 			}
970 		};
971 
972 		boolean containsMeasure = false;
973 
974 		List<Member> firstMembers = positions.get(0).getMembers();
975 
976 		int index = 0;
977 		for (Member member : firstMembers) {
978 			if (member instanceof Measure) {
979 				containsMeasure = true;
980 				break;
981 			}
982 
983 			index++;
984 		}
985 
986 		AggregatorFactory aggregatorFactory = getAggregatorFactory();
987 
988 		List<String> aggregatorNames = null;
989 		List<String> hierarchyAggregatorNames = null;
990 		List<String> memberAggregatorNames = null;
991 
992 		if (aggregatorFactory != null
993 				&& (!containsMeasure || index == firstMembers.size() - 1)) {
994 			aggregatorNames = getAggregators(axis, AggregatorPosition.Grand);
995 			hierarchyAggregatorNames = getAggregators(axis,
996 					AggregatorPosition.Hierarchy);
997 			memberAggregatorNames = getAggregators(axis,
998 					AggregatorPosition.Member);
999 		}
1000 
1001 		if (aggregatorNames == null) {
1002 			aggregatorNames = Collections.emptyList();
1003 		}
1004 
1005 		if (hierarchyAggregatorNames == null) {
1006 			hierarchyAggregatorNames = Collections.emptyList();
1007 		}
1008 
1009 		if (memberAggregatorNames == null) {
1010 			memberAggregatorNames = Collections.emptyList();
1011 		}
1012 
1013 		TableAxisContext nodeContext = new TableAxisContext(axis, hierarchies,
1014 				levelsMap, aggregatorMap, this);
1015 
1016 		TableHeaderNode axisRoot = new TableHeaderNode(nodeContext);
1017 
1018 		Set<Measure> grandTotalMeasures = new LinkedHashSet<Measure>();
1019 		Set<Measure> totalMeasures = new LinkedHashSet<Measure>();
1020 
1021 		Map<Hierarchy, List<AggregationTarget>> memberParentMap = new HashMap<Hierarchy, List<AggregationTarget>>();
1022 
1023 		Position lastPosition = null;
1024 
1025 		for (Position position : positions) {
1026 			TableHeaderNode lastChild = null;
1027 
1028 			List<Member> members = position.getMembers();
1029 
1030 			int memberCount = members.size();
1031 
1032 			for (int i = memberCount - 1; i >= 0; i--) {
1033 				Member member = members.get(i);
1034 
1035 				if (member instanceof Measure) {
1036 					Measure measure = (Measure) member;
1037 
1038 					if (!totalMeasures.contains(measure)) {
1039 						totalMeasures.add(measure);
1040 					}
1041 
1042 					if (!grandTotalMeasures.contains(measure)) {
1043 						grandTotalMeasures.add(measure);
1044 					}
1045 				} else if (member != null && member.getDepth() > 0
1046 						&& nodeContext.getParentMember(member) == null) {
1047 					member = new RaggedMemberWrapper(member, model.getCube());
1048 				}
1049 
1050 				if (hierarchies.size() < memberCount) {
1051 					hierarchies.add(0, member.getHierarchy());
1052 				}
1053 
1054 				List<Level> levels = levelsMap.get(member.getHierarchy());
1055 
1056 				if (levels == null) {
1057 					levels = new ArrayList<Level>();
1058 					levelsMap.put(member.getHierarchy(), levels);
1059 				}
1060 
1061 				if (!levels.contains(member.getLevel())) {
1062 					levels.add(0, member.getLevel());
1063 					Collections.sort(levels, levelComparator);
1064 				}
1065 
1066 				TableHeaderNode childNode = new TableHeaderNode(nodeContext);
1067 
1068 				childNode.setMember(member);
1069 				childNode.setHierarchy(member.getHierarchy());
1070 				childNode.setPosition(position);
1071 
1072 				if (lastChild != null) {
1073 					childNode.addChild(lastChild);
1074 				}
1075 
1076 				lastChild = childNode;
1077 
1078 				if (!hierarchyAggregatorNames.isEmpty() && lastPosition != null) {
1079 					int start = memberCount - 1;
1080 
1081 					if (containsMeasure) {
1082 						start--;
1083 					}
1084 
1085 					if (i < start) {
1086 						Member lastMember = lastPosition.getMembers().get(i);
1087 
1088 						if (OlapUtils.equals(lastMember.getHierarchy(),
1089 								member.getHierarchy())
1090 								&& !OlapUtils.equals(lastMember, member)
1091 								|| !OlapUtils.equals(position, lastPosition, i)) {
1092 							for (String aggregatorName : hierarchyAggregatorNames) {
1093 								createAggregators(
1094 										aggregatorName,
1095 										nodeContext,
1096 										hierarchyAggregators,
1097 										axisRoot,
1098 										null,
1099 										lastPosition.getMembers().subList(0,
1100 												i + 1), totalMeasures);
1101 							}
1102 						}
1103 					}
1104 				}
1105 
1106 				if (!memberAggregatorNames.isEmpty()) {
1107 					List<AggregationTarget> memberParents = memberParentMap
1108 							.get(member.getHierarchy());
1109 
1110 					if (memberParents == null) {
1111 						memberParents = new ArrayList<AggregationTarget>();
1112 						memberParentMap.put(member.getHierarchy(),
1113 								memberParents);
1114 					}
1115 
1116 					AggregationTarget lastSibling = null;
1117 
1118 					if (!memberParents.isEmpty()) {
1119 						lastSibling = memberParents
1120 								.get(memberParents.size() - 1);
1121 					}
1122 
1123 					Member parentMember;
1124 
1125 					if (member instanceof RaggedMemberWrapper) {
1126 						parentMember = ((RaggedMemberWrapper) member)
1127 								.getTopMember();
1128 					} else {
1129 						parentMember = axisRoot.getReference().getParentMember(
1130 								member);
1131 					}
1132 
1133 					if (parentMember != null) {
1134 						if (lastSibling == null
1135 								|| axisRoot.getReference()
1136 										.getAncestorMembers(parentMember)
1137 										.contains(lastSibling.getParent())) {
1138 							memberParents.add(new AggregationTarget(
1139 									parentMember, member.getLevel()));
1140 						} else if (!OlapUtils.equals(parentMember,
1141 								lastSibling.getParent())) {
1142 							while (!memberParents.isEmpty()) {
1143 								int lastIndex = memberParents.size() - 1;
1144 
1145 								AggregationTarget lastParent = memberParents
1146 										.get(lastIndex);
1147 
1148 								if (OlapUtils.equals(lastParent.getParent(),
1149 										parentMember)) {
1150 									break;
1151 								}
1152 
1153 								memberParents.remove(lastIndex);
1154 
1155 								List<Member> path = new ArrayList<Member>(
1156 										lastPosition.getMembers().subList(0, i));
1157 								path.add(lastParent.getParent());
1158 
1159 								Level parentLevel = lastParent.getParent()
1160 										.getLevel();
1161 								if (!levels.contains(parentLevel)) {
1162 									levels.add(0, parentLevel);
1163 									Collections.sort(levels, levelComparator);
1164 								}
1165 
1166 								for (String aggregatorName : memberAggregatorNames) {
1167 									createAggregators(aggregatorName,
1168 											nodeContext, memberAggregators,
1169 											axisRoot, lastParent.getLevel(),
1170 											path, totalMeasures);
1171 								}
1172 							}
1173 						}
1174 					}
1175 
1176 					if (lastPosition != null
1177 							&& !OlapUtils.equals(position, lastPosition, i)) {
1178 						Hierarchy hierarchy = nodeContext.getHierarchies().get(
1179 								i);
1180 
1181 						Level rootLevel = nodeContext.getLevels(hierarchy).get(
1182 								0);
1183 
1184 						for (AggregationTarget target : memberParents) {
1185 							Member memberParent = target.getParent();
1186 
1187 							if (memberParent.getLevel().getDepth() < rootLevel
1188 									.getDepth()) {
1189 								continue;
1190 							}
1191 
1192 							List<Member> path = new ArrayList<Member>(
1193 									lastPosition.getMembers().subList(0, i));
1194 							path.add(memberParent);
1195 
1196 							for (String aggregatorName : memberAggregatorNames) {
1197 								createAggregators(aggregatorName, nodeContext,
1198 										memberAggregators, axisRoot,
1199 										target.getLevel(), path, totalMeasures);
1200 							}
1201 						}
1202 
1203 						memberParents.clear();
1204 					}
1205 				}
1206 			}
1207 
1208 			if (lastChild != null) {
1209 				axisRoot.addChild(lastChild);
1210 			}
1211 
1212 			lastPosition = position;
1213 		}
1214 
1215 		if (lastPosition != null) {
1216 			int memberCount = lastPosition.getMembers().size();
1217 
1218 			int start = memberCount - 1;
1219 
1220 			if (containsMeasure) {
1221 				start--;
1222 			}
1223 
1224 			for (int i = start; i >= 0; i--) {
1225 				if (!memberAggregatorNames.isEmpty()) {
1226 					Hierarchy hierarchy = nodeContext.getHierarchies().get(i);
1227 
1228 					Level rootLevel = nodeContext.getLevels(hierarchy).get(0);
1229 
1230 					List<AggregationTarget> memberParents = memberParentMap
1231 							.get(hierarchy);
1232 
1233 					for (AggregationTarget target : memberParents) {
1234 						Member member = target.getParent();
1235 
1236 						if (member.getLevel().getDepth() < rootLevel.getDepth()) {
1237 							continue;
1238 						}
1239 
1240 						List<Member> path = new ArrayList<Member>(lastPosition
1241 								.getMembers().subList(0, i));
1242 						path.add(member);
1243 
1244 						for (String aggregatorName : memberAggregatorNames) {
1245 							createAggregators(aggregatorName, nodeContext,
1246 									memberAggregators, axisRoot,
1247 									target.getLevel(), path, totalMeasures);
1248 						}
1249 					}
1250 				}
1251 
1252 				if (!hierarchyAggregatorNames.isEmpty()) {
1253 					for (String aggregatorName : hierarchyAggregatorNames) {
1254 						createAggregators(aggregatorName, nodeContext,
1255 								hierarchyAggregators, axisRoot, null,
1256 								lastPosition.getMembers().subList(0, i),
1257 								totalMeasures);
1258 					}
1259 				}
1260 			}
1261 
1262 			if (!aggregatorNames.isEmpty()) {
1263 				List<Member> members = Collections.emptyList();
1264 
1265 				for (String aggregatorName : aggregatorNames) {
1266 					createAggregators(aggregatorName, nodeContext, aggregators,
1267 							axisRoot, null, members, grandTotalMeasures);
1268 				}
1269 			}
1270 		}
1271 
1272 		if (logger.isDebugEnabled()) {
1273 			logger.debug("Original axis tree root for " + axis);
1274 
1275 			axisRoot.walkTree(new TreeNodeCallback<TableAxisContext>() {
1276 
1277 				@Override
1278 				public int handleTreeNode(TreeNode<TableAxisContext> node) {
1279 					String label = node.toString();
1280 					logger.trace(StringUtils.leftPad(label, node.getLevel()
1281 							+ label.length(), ' '));
1282 
1283 					return CONTINUE;
1284 				}
1285 			});
1286 		}
1287 
1288 		return axisRoot;
1289 	}
1290 
1291 	/**
1292 	 * @param aggregatorName
1293 	 * @param context
1294 	 * @param aggregators
1295 	 * @param axisRoot
1296 	 * @param level
1297 	 * @param members
1298 	 * @param measures
1299 	 */
1300 	private void createAggregators(String aggregatorName,
1301 			TableAxisContext context, List<Aggregator> aggregators,
1302 			TableHeaderNode axisRoot, Level level, List<Member> members,
1303 			Set<Measure> measures) {
1304 		AggregatorFactory factory = getAggregatorFactory();
1305 
1306 		if (measures.isEmpty()) {
1307 			Aggregator aggregator = factory.createAggregator(aggregatorName,
1308 					context.getAxis(), members, level, null);
1309 			if (aggregator != null) {
1310 				aggregators.add(aggregator);
1311 
1312 				TableHeaderNode aggNode = createAggregationNode(context,
1313 						aggregator);
1314 				axisRoot.addChild(aggNode);
1315 			}
1316 		} else {
1317 			for (Measure measure : measures) {
1318 				Aggregator aggregator = factory.createAggregator(
1319 						aggregatorName, context.getAxis(), members, level,
1320 						measure);
1321 
1322 				if (aggregator != null) {
1323 					aggregators.add(aggregator);
1324 
1325 					TableHeaderNode aggNode = createAggregationNode(context,
1326 							aggregator);
1327 					axisRoot.addChild(aggNode);
1328 				}
1329 			}
1330 		}
1331 	}
1332 
1333 	/**
1334 	 * @param nodeContext
1335 	 * @param aggregator
1336 	 * @return
1337 	 */
1338 	protected TableHeaderNode createAggregationNode(
1339 			TableAxisContext nodeContext, Aggregator aggregator) {
1340 		TableHeaderNode result = null;
1341 
1342 		TableHeaderNode parent = null;
1343 
1344 		List<Member> members = new ArrayList<Member>(aggregator.getMembers());
1345 
1346 		Position position = new AggregatePosition(members);
1347 
1348 		for (Member member : aggregator.getMembers()) {
1349 			TableHeaderNode node = new TableHeaderNode(nodeContext);
1350 
1351 			node.setAggregation(true);
1352 			node.setMember(member);
1353 			node.setPosition(position);
1354 			node.setHierarchy(member.getHierarchy());
1355 
1356 			if (result == null) {
1357 				result = node;
1358 			}
1359 
1360 			if (parent != null) {
1361 				parent.addChild(node);
1362 			}
1363 
1364 			parent = node;
1365 		}
1366 
1367 		if (parent != null && aggregator.getLevel() != null
1368 				&& !getShowParentMembers()) {
1369 			parent.setAggregator(aggregator);
1370 		}
1371 
1372 		int index = Math.min(nodeContext.getHierarchies().size() - 1,
1373 				members.size());
1374 
1375 		Hierarchy hierarchy;
1376 
1377 		if (aggregator.getLevel() == null) {
1378 			hierarchy = nodeContext.getHierarchies().get(index);
1379 		} else {
1380 			hierarchy = aggregator.getLevel().getHierarchy();
1381 		}
1382 
1383 		if (aggregator.getLevel() == null || getShowParentMembers()) {
1384 			TableHeaderNode node = new TableHeaderNode(nodeContext);
1385 			node.setAggregation(true);
1386 			node.setAggregator(aggregator);
1387 			node.setPosition(position);
1388 			node.setHierarchy(hierarchy);
1389 
1390 			if (parent != null) {
1391 				parent.addChild(node);
1392 			}
1393 
1394 			if (result == null) {
1395 				result = node;
1396 			}
1397 
1398 			parent = node;
1399 		}
1400 
1401 		Measure measure = aggregator.getMeasure();
1402 
1403 		if (measure != null) {
1404 			TableHeaderNode measureNode = new TableHeaderNode(nodeContext);
1405 
1406 			measureNode.setAggregation(true);
1407 			measureNode.setAggregator(aggregator);
1408 			measureNode.setPosition(position);
1409 			measureNode.setMember(measure);
1410 			measureNode.setHierarchy(measure.getHierarchy());
1411 
1412 			parent.addChild(measureNode);
1413 
1414 			members.add(measure);
1415 		}
1416 
1417 		return result;
1418 	}
1419 
1420 	/**
1421 	 * @param model
1422 	 * @param axis
1423 	 * @param node
1424 	 */
1425 	protected void configureAxisTree(PivotModel model, Axis axis,
1426 			TableHeaderNode node) {
1427 		if (getShowDimensionTitle() && axis.equals(Axis.COLUMNS)) {
1428 			node.addHierarhcyHeaders();
1429 		}
1430 
1431 		if (getShowParentMembers()) {
1432 			node.addParentMemberHeaders();
1433 		}
1434 
1435 		if (!getHideSpans()) {
1436 			node.mergeChildren();
1437 		}
1438 
1439 		if (getPropertyCollector() != null) {
1440 			node.addMemberProperties();
1441 		}
1442 
1443 		if (logger.isDebugEnabled()) {
1444 			logger.debug("Configured axis tree root for " + axis);
1445 
1446 			node.walkTree(new TreeNodeCallback<TableAxisContext>() {
1447 
1448 				@Override
1449 				public int handleTreeNode(TreeNode<TableAxisContext> node) {
1450 					String label = node.toString();
1451 					logger.debug(StringUtils.leftPad(label, node.getLevel()
1452 							+ label.length(), ' '));
1453 
1454 					return CONTINUE;
1455 				}
1456 			});
1457 		}
1458 	}
1459 
1460 	/**
1461 	 * @param model
1462 	 * @param axis
1463 	 * @param node
1464 	 */
1465 	protected void invalidateAxisTree(PivotModel model, Axis axis,
1466 			TableHeaderNode node) {
1467 		node.walkChildren(new TreeNodeCallback<TableAxisContext>() {
1468 
1469 			@Override
1470 			public int handleTreeNode(TreeNode<TableAxisContext> node) {
1471 				TableHeaderNode headerNode = (TableHeaderNode) node;
1472 				headerNode.clearCache();
1473 				return TreeNodeCallback.CONTINUE;
1474 			}
1475 		});
1476 	}
1477 
1478 	static class AggregationTarget {
1479 
1480 		private Member parent;
1481 
1482 		private Level level;
1483 
1484 		/**
1485 		 * @param parent
1486 		 * @param level
1487 		 */
1488 		AggregationTarget(Member parent, Level level) {
1489 			this.parent = parent;
1490 			this.level = level;
1491 		}
1492 
1493 		/**
1494 		 * @return the parent
1495 		 */
1496 		public Member getParent() {
1497 			return parent;
1498 		}
1499 
1500 		/**
1501 		 * @return the level
1502 		 */
1503 		public Level getLevel() {
1504 			return level;
1505 		}
1506 	}
1507 
1508 	static class AggregatePosition implements Position {
1509 
1510 		private List<Member> members;
1511 
1512 		/**
1513 		 * @param members
1514 		 */
1515 		AggregatePosition(List<Member> members) {
1516 			this.members = members;
1517 		}
1518 
1519 		/**
1520 		 * @see org.olap4j.Position#getMembers()
1521 		 */
1522 		@Override
1523 		public List<Member> getMembers() {
1524 			return members;
1525 		}
1526 
1527 		/**
1528 		 * @see org.olap4j.Position#getOrdinal()
1529 		 */
1530 		@Override
1531 		public int getOrdinal() {
1532 			return -1;
1533 		}
1534 
1535 		/**
1536 		 * @see java.lang.Object#hashCode()
1537 		 */
1538 		@Override
1539 		public int hashCode() {
1540 			return 31 + members.hashCode();
1541 		}
1542 
1543 		/**
1544 		 * @see java.lang.Object#equals(java.lang.Object)
1545 		 */
1546 		@Override
1547 		public boolean equals(Object obj) {
1548 			if (this == obj) {
1549 				return true;
1550 			} else if (obj == null) {
1551 				return false;
1552 			} else if (getClass() != obj.getClass()) {
1553 				return false;
1554 			}
1555 
1556 			AggregatePosition other = (AggregatePosition) obj;
1557 			return members.equals(other.members);
1558 		}
1559 	}
1560 
1561 	/**
1562 	 * @param context
1563 	 * @param callback
1564 	 */
1565 	protected void renderFilter(TableRenderContext context,
1566 			TableRenderCallback callback) {
1567 		ChangeSlicer transform = context.getModel().getTransform(
1568 				ChangeSlicer.class);
1569 
1570 		List<Hierarchy> hierarchies = transform.getHierarchies();
1571 		if (hierarchies.isEmpty()) {
1572 			return;
1573 		}
1574 
1575 		context.setAxis(Axis.FILTER);
1576 		context.setHierarchy(null);
1577 		context.setLevel(null);
1578 		context.setMember(null);
1579 		context.setCell(null);
1580 		context.setAggregator(null);
1581 		context.setPosition(null);
1582 
1583 		callback.startTable(context);
1584 
1585 		callback.startHeader(context);
1586 		callback.endHeader(context);
1587 
1588 		int rowIndex = 0;
1589 
1590 		callback.startBody(context);
1591 
1592 		for (Hierarchy hierarchy : hierarchies) {
1593 			List<Member> members = transform.getSlicer(hierarchy);
1594 
1595 			context.setHierarchy(hierarchy);
1596 			context.setMember(null);
1597 
1598 			context.setRowIndex(rowIndex);
1599 			context.setColIndex(0);
1600 
1601 			callback.startRow(context);
1602 
1603 			context.setCellType(LABEL);
1604 
1605 			context.setColSpan(1);
1606 			context.setRowSpan(Math.max(1, members.size()));
1607 
1608 			callback.startCell(context);
1609 			callback.renderCommands(context, getCommands(context));
1610 			callback.renderContent(context, getLabel(context));
1611 			callback.endCell(context);
1612 
1613 			boolean firstRow = true;
1614 
1615 			for (Member member : members) {
1616 				if (firstRow) {
1617 					firstRow = false;
1618 				} else {
1619 					callback.endRow(context);
1620 
1621 					context.setRowIndex(++rowIndex);
1622 
1623 					callback.startRow(context);
1624 				}
1625 
1626 				context.setColIndex(1);
1627 
1628 				context.setMember(member);
1629 				context.setLevel(member.getLevel());
1630 
1631 				context.setRowSpan(1);
1632 
1633 				context.setCellType(VALUE);
1634 
1635 				callback.startCell(context);
1636 				callback.renderCommands(context, getCommands(context));
1637 				callback.renderContent(context, getLabel(context));
1638 				callback.endCell(context);
1639 			}
1640 
1641 			callback.endRow(context);
1642 
1643 			rowIndex++;
1644 		}
1645 
1646 		callback.endBody(context);
1647 		callback.endTable(context);
1648 	}
1649 }