001: /*********************************************************************************
002: * The contents of this file are subject to the OpenI Public License Version 1.0
003: * ("License"); You may not use this file except in compliance with the
004: * License. You may obtain a copy of the License at
005: * http://www.openi.org/docs/LICENSE.txt
006: *
007: * Software distributed under the License is distributed on an "AS IS" basis,
008: * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for
009: * the specific language governing rights and limitations under the License.
010: *
011: * The Original Code is: OpenI Open Source
012: *
013: * The Initial Developer of the Original Code is Loyalty Matrix, Inc.
014: * Portions created by Loyalty Matrix, Inc. are
015: * Copyright (C) 2005 Loyalty Matrix, Inc.; All Rights Reserved.
016: *
017: * Contributor(s): i18n : Pedro Casals Fradera (2006/06/24)
018: *
019: ********************************************************************************/package org.openi.xmla;
020:
021: import com.tonbeller.jpivot.olap.model.Cell;
022: import com.tonbeller.jpivot.olap.model.Member;
023: import com.tonbeller.jpivot.olap.model.OlapException;
024: import com.tonbeller.jpivot.olap.model.OlapModel;
025: import com.tonbeller.jpivot.olap.model.Position;
026: import com.tonbeller.jpivot.olap.navi.MemberTree;
027:
028: import org.apache.log4j.Logger;
029:
030: import org.jfree.data.category.DefaultCategoryDataset;
031: import org.jfree.data.time.Day;
032: import org.jfree.data.time.Month;
033: import org.jfree.data.time.Quarter;
034: import org.jfree.data.time.RegularTimePeriod;
035: import org.jfree.data.time.TimeSeries;
036: import org.jfree.data.time.TimeSeriesCollection;
037: import org.jfree.data.xy.XYDataset;
038:
039: import java.text.DecimalFormat;
040: import java.text.DecimalFormatSymbols;
041: import java.text.NumberFormat;
042: import java.text.ParseException;
043: import java.text.SimpleDateFormat;
044:
045: import java.util.Calendar;
046: import java.util.Date;
047: import java.util.HashMap;
048: import java.util.List;
049: import java.util.Locale;
050:
051: /**
052: * Takes an olap model, adapts it to a jfreechart dataset
053: */
054: public class DatasetAdapter {
055: private static Logger logger = Logger
056: .getLogger(DatasetAdapter.class);
057: private Locale locale;
058: private NumberFormat numberFormatter;
059:
060: /**
061: * @param locale
062: */
063: public DatasetAdapter(Locale locale) {
064: this .locale = locale;
065: }
066:
067: /**
068: * @param dataset
069: * @return
070: */
071: public DefaultCategoryDataset buildCategoryDataset(
072: OlapModel olapModel) throws OlapException {
073: long start = System.currentTimeMillis();
074:
075: DefaultCategoryDataset dataset = null;
076: int dimCount = olapModel.getResult().getAxes().length;
077:
078: switch (dimCount) {
079: case 1:
080: logger.info("1-dim data");
081: dataset = build1dimDataset(olapModel);
082:
083: break;
084:
085: case 2:
086: logger.info("2-dim data");
087: dataset = build2dimDataset(olapModel);
088:
089: break;
090:
091: default:
092: logger.error("less than 1 or more than 2 dimensions");
093: throw new IllegalArgumentException(
094: "ChartRenderer requires a 1 or 2 dimensional result");
095: }
096:
097: logger.debug("built datset in: "
098: + (System.currentTimeMillis() - start) + "ms");
099:
100: return dataset;
101: }
102:
103: /**
104: * Build a jfreechart CategoryDataset with a single series
105: * @param result TODO
106: *
107: */
108: private DefaultCategoryDataset build1dimDataset(OlapModel olapModel)
109: throws OlapException {
110: DefaultCategoryDataset dataset = new DefaultCategoryDataset();
111:
112: // column axis
113: List columnPositions = olapModel.getResult().getAxes()[0]
114: .getPositions();
115: int colCount = columnPositions.size();
116:
117: // cells
118: List cells = olapModel.getResult().getCells();
119:
120: String series = "Series";
121:
122: // loop on column positions
123: for (int i = 0; i < colCount; i++) {
124: Member[] colMembers = ((Position) columnPositions.get(i))
125: .getMembers();
126:
127: StringBuffer key = new StringBuffer();
128:
129: // loop on col position members
130: for (int j = 0; j < colMembers.length; j++) {
131: // build up composite name for this row
132: key.append(colMembers[j].getLabel() + ".");
133: }
134:
135: dataset.addValue(getNumberValue((Cell) cells.get(i)),
136: series, key.toString());
137: }
138:
139: return dataset;
140: }
141:
142: /**
143: * Build a jfreechart CategoryDataset with multiple series
144: * @param olapModel
145: * @param result TODO
146: *
147: */
148: private DefaultCategoryDataset build2dimDataset(OlapModel olapModel)
149: throws OlapException {
150: DefaultCategoryDataset dataset = new DefaultCategoryDataset();
151:
152: // column axis
153: List columnPositions = olapModel.getResult().getAxes()[0]
154: .getPositions(); //ladX.getPositions();
155: int colCount = columnPositions.size();
156:
157: // row axis
158: List rowPositions = olapModel.getResult().getAxes()[1]
159: .getPositions(); //ladY.getPositions();
160: int rowCount = rowPositions.size();
161: List cells = olapModel.getResult().getCells();
162:
163: // get the full member tree
164: MemberTree myTree = ((MemberTree) olapModel
165: .getExtension(MemberTree.ID));
166:
167: // for each column, starting with the bottom member, progress up the mmeber chain until the root is reached
168: // keep track of the levels and hierarchies to avoid duplicates on level or hierarchys.
169: // *note: keeping track of the levels might be just extra work, I don't know if they CAN be repeated.
170: // if not, that logic can be easily removed (see buildName - above)
171: // For each hierarchy, If a root member is reached (getRootDistance()=0), then only include it if there have been no other
172: // lower level members already added:
173: // ie. All_dim1.dim1_lvl1.dim1_lvl2.All_dim2.dim2_lvl1 renders as dim1_lvl1.dim1_lvl2.dim2_lvl1
174: // whereas All_dim1.All_dim2 renders as the same.
175: // The important part is that we include each parent on the way up, to ensure a unique name to
176: // place in the map for the dataset (no longer overwriting each other)
177:
178: for (int i = 0; i < colCount; i++) {
179: Position p = (Position) columnPositions.get(i);
180: Member[] colMembers = p.getMembers();
181:
182: // build the label name for this column
183: String label = buildName(myTree, colMembers);
184:
185: // For each row, use the same logic to build a unique key for each data item
186: for (int k = 0; k < rowCount; k++) {
187: Position rp = (Position) rowPositions.get(k);
188: Member[] rowMembers = rp.getMembers();
189:
190: // build key name
191: String key = buildName(myTree, rowMembers);
192:
193: Cell cell = (Cell) cells.get((k * colCount) + i);
194:
195: dataset.addValue(getNumberValue(cell),
196: label.toString(), key.toString());
197: }
198: }
199:
200: return dataset;
201: }
202:
203: /**
204: * Get cell value as a Number. Parses the cell value string
205: * using the locale set in this.locale.
206: * @param cell
207: * @return value as Number (can be null)
208: */
209: private Number getNumberValue(Cell cell) {
210: Number value = (Number) cell.getValue();
211:
212: //added to fix data format bug in range axis
213: if (numberFormatter == null) {
214: if ((cell.getFormattedValue() != null)
215: && (cell.getFormattedValue() != "")) {
216: String fmtValue = cell.getFormattedValue();
217: fmtValue = fmtValue.trim();
218:
219: DecimalFormatSymbols dfs = new DecimalFormatSymbols(
220: this .locale);
221: if (fmtValue.endsWith(String.valueOf(dfs.getPercent()))) {
222: numberFormatter = NumberFormat
223: .getPercentInstance(this .locale);
224: } else if (fmtValue.indexOf(dfs.getCurrencySymbol()) >= 0) {
225: numberFormatter = NumberFormat
226: .getCurrencyInstance(this .locale);
227: numberFormatter.setMaximumFractionDigits(0);
228: numberFormatter.setMinimumFractionDigits(0);
229: } else if (value instanceof Double) {
230: numberFormatter = NumberFormat
231: .getNumberInstance(this .locale);
232: } else {
233: numberFormatter = NumberFormat
234: .getIntegerInstance(this .locale);
235: }
236: } else {
237: if (value instanceof Double) {
238: numberFormatter = NumberFormat
239: .getNumberInstance(this .locale);
240: } else {
241: numberFormatter = NumberFormat
242: .getIntegerInstance(this .locale);
243: }
244: }
245: }
246:
247: return value;
248: }
249:
250: /**
251: * Get a unique name string for a dataitem derived from the member chain
252: *
253: * @param myTree (full member tree)
254: * @param members - the list to be processed (either X/Y axis)
255: * @return retValue as String
256: */
257: private String buildName(MemberTree myTree, Member[] members) {
258: String retValue = new String();
259: HashMap levelMap = new HashMap();
260: HashMap hierarchyMap = new HashMap();
261:
262: for (int j = members.length - 1; j >= 0; j--) {
263: Member member = members[j];
264:
265: while (member != null) {
266: // only process if no other items from this level processed - should not be duplicates!
267: if (!levelMap.containsValue(member.getLevel())) {
268: levelMap.put(member.getLevel().toString(), member
269: .getLevel());
270:
271: if (member.getRootDistance() == 0) {
272: // if root member, only add to name if no other members of the hierarchy are already added
273: if (!hierarchyMap.containsValue(member
274: .getLevel().getHierarchy())
275: || (myTree.getRootMembers(member
276: .getLevel().getHierarchy()).length > 1)) {
277: hierarchyMap.put(member.getLevel()
278: .getHierarchy().toString(), member
279: .getLevel().getHierarchy());
280: retValue = member.getLabel() + "."
281: + retValue;
282: }
283: } else {
284: hierarchyMap.put(member.getLevel()
285: .getHierarchy().toString(), member
286: .getLevel().getHierarchy());
287: retValue = member.getLabel() + "." + retValue;
288: }
289: }
290:
291: member = myTree.getParent(member);
292: }
293: }
294:
295: return retValue;
296: }
297:
298: /**
299: * Very experimental, notice that the time dimension needs to be on the rows,
300: * and there can be no other dimensions on rows.
301: *
302: * Each dimension on the column will have it's own series, added to the TimeSeriesCollection
303: */
304: public XYDataset buildXYDataset(OlapModel olapModel)
305: throws OlapException {
306: long start = System.currentTimeMillis();
307:
308: TimeSeriesCollection dataset = new TimeSeriesCollection();
309:
310: // column axis
311: List columnPositions = olapModel.getResult().getAxes()[0]
312: .getPositions(); //ladX.getPositions();
313: int colCount = columnPositions.size();
314:
315: // row axis
316: List rowPositions = olapModel.getResult().getAxes()[1]
317: .getPositions(); //ladY.getPositions();
318: int rowCount = rowPositions.size();
319: List cells = olapModel.getResult().getCells();
320:
321: // get the full member tree
322: MemberTree myTree = ((MemberTree) olapModel
323: .getExtension(MemberTree.ID));
324:
325: // each column gets its own time series
326: for (int i = 0; i < colCount; i++) {
327: Position p = (Position) columnPositions.get(i);
328: Member[] colMembers = p.getMembers();
329:
330: // build the label name for this column
331: String label = buildName(myTree, colMembers);
332: TimeSeries series = createTimeSeries(label,
333: parseRootLevel(rowPositions));
334:
335: for (int k = 0; k < rowCount; k++) {
336: Position rp = (Position) rowPositions.get(k);
337: Member[] rowMembers = rp.getMembers();
338: Cell cell = (Cell) cells.get((k * colCount) + i);
339:
340: try {
341: // TODO:
342: // should determine if this is a month, or day, year
343: // similar to buildName, need a buildMonth and/or buildDate,
344: // allowing date hierarchies.
345: RegularTimePeriod current = null;
346:
347: if (series.getTimePeriodClass() == Quarter.class) {
348: current = createQuarter(myTree, rowMembers);
349: } else if (series.getTimePeriodClass() == Month.class) {
350: current = createMonth(myTree, rowMembers);
351: } else if (series.getTimePeriodClass() == Day.class) {
352: current = createDay(myTree, rowMembers);
353: }
354:
355: series.add(current, getNumberValue(cell));
356: } catch (ParseException e) {
357: // logger.error(e);
358: }
359: }
360:
361: dataset.addSeries(series);
362: }
363:
364: dataset.setDomainIsPointsInTime(true);
365: logger.debug("created XY Dataset in: "
366: + (System.currentTimeMillis() - start) + " ms");
367:
368: return dataset;
369: }
370:
371: private TimeSeries createTimeSeries(String label, String rootLevel) {
372: logger.debug("creating timeseries for: " + rootLevel);
373:
374: TimeSeries series = null;
375:
376: if ("quarter".equalsIgnoreCase(rootLevel)) {
377: series = new TimeSeries(label, Quarter.class);
378: } else if ("month".equalsIgnoreCase(rootLevel)) {
379: series = new TimeSeries(label, Month.class);
380: } else if ("day".equalsIgnoreCase(rootLevel)) {
381: series = new TimeSeries(label, Day.class);
382: }
383:
384: return series;
385: }
386:
387: private RegularTimePeriod createQuarter(MemberTree memberTree,
388: Member[] members) {
389: String year = memberTree.getParent(members[0]).getLabel();
390: int quarter = 1;
391: String memberLabel = members[0].getLabel();
392:
393: // month = parseMonth(memberLabel);
394: if ("Q1".equalsIgnoreCase(memberLabel)) {
395: quarter = 1;
396: } else if ("Q2".equalsIgnoreCase(memberLabel)) {
397: quarter = 2;
398: } else if ("Q3".equalsIgnoreCase(memberLabel)) {
399: quarter = 3;
400: } else if ("Q4".equalsIgnoreCase(memberLabel)) {
401: quarter = 4;
402: }
403:
404: return new Quarter(quarter, Integer.parseInt(year));
405: }
406:
407: /**
408: * @return month object - a org.jfree.data.time.Month (RegularTimePeriod)
409: */
410: private Month createMonth(MemberTree memberTree, Member[] members)
411: throws ParseException {
412: String year = memberTree.getParent(members[0]).getLabel();
413: int month = 1;
414: String memberLabel = members[0].getLabel();
415: month = parseMonth(memberLabel);
416:
417: return new Month(month, Integer.parseInt(year));
418: }
419:
420: private RegularTimePeriod createDay(MemberTree memberTree,
421: Member[] members) {
422: int day = 1;
423: String dayLabel = members[0].getLabel();
424: day = Integer.parseInt(dayLabel);
425:
426: int month = 1;
427: String monthLabel = memberTree.getParent(members[0]).getLabel();
428: month = parseMonth(monthLabel);
429:
430: int year = 1900;
431: String yearLabel = memberTree.getParent(members[0]).getLabel();
432:
433: return new Day(day, month, year);
434: }
435:
436: private int parseMonth(String memberLabel) {
437: Integer month = null;
438:
439: //try to parse assuming int:
440: try {
441: month = new Integer(memberLabel);
442: } catch (NumberFormatException e) {
443: // logger.debug("could not parse as int");
444: }
445:
446: // try string?
447: if (month == null) {
448: try {
449: SimpleDateFormat formatter = new SimpleDateFormat("MMM");
450: Date date = formatter.parse(memberLabel);
451: month = new Integer(date.getMonth() + 1);
452: } catch (ParseException e) {
453: // logger.debug(e);
454: }
455: }
456:
457: return month.intValue();
458: }
459:
460: private String parseRootLevel(List rowPositions) {
461: String rootLevel = null;
462:
463: try {
464: rootLevel = ((Position) rowPositions.get(0)).getMembers()[0]
465: .getLevel().getLabel();
466: } catch (Exception e) {
467: logger.debug(e);
468: }
469:
470: return rootLevel;
471: }
472:
473: public NumberFormat getNumberFormatter() {
474: return numberFormatter;
475: }
476: }
|