001 /*
002 // $Id: //open/mondrian-release/3.0/src/main/mondrian/rolap/RolapCube.java#6 $
003 // This software is subject to the terms of the Common Public License
004 // Agreement, available at the following URL:
005 // http://www.opensource.org/licenses/cpl.html.
006 // Copyright (C) 2001-2002 Kana Software, Inc.
007 // Copyright (C) 2001-2007 Julian Hyde and others
008 // All Rights Reserved.
009 // You must accept the terms of that agreement to use this software.
010 //
011 // jhyde, 10 August, 2001
012 */
013
014 package mondrian.rolap;
015
016 import mondrian.mdx.MdxVisitorImpl;
017 import mondrian.mdx.MemberExpr;
018 import mondrian.olap.*;
019 import mondrian.resource.MondrianResource;
020 import mondrian.rolap.aggmatcher.ExplicitRules;
021 import mondrian.rolap.cache.SoftSmartCache;
022 import org.apache.log4j.Logger;
023 import org.eigenbase.xom.*;
024 import org.eigenbase.xom.Parser;
025
026 import java.lang.reflect.Constructor;
027 import java.util.*;
028
029 /**
030 * <code>RolapCube</code> implements {@link Cube} for a ROLAP database.
031 *
032 * @author jhyde
033 * @since 10 August, 2001
034 * @version $Id: //open/mondrian-release/3.0/src/main/mondrian/rolap/RolapCube.java#6 $
035 */
036 public class RolapCube extends CubeBase {
037
038 private static final Logger LOGGER = Logger.getLogger(RolapCube.class);
039
040 private final RolapSchema schema;
041 private final RolapHierarchy measuresHierarchy;
042
043 /** For SQL generator. Fact table. */
044 final MondrianDef.Relation fact;
045
046 /** Schema reader which can see this cube and nothing else. */
047 private SchemaReader schemaReader;
048
049 /**
050 * List of calculated members.
051 */
052 private Formula[] calculatedMembers;
053
054 /**
055 * Role based cache of calculated members
056 */
057 private final SoftSmartCache<Role, List<Member>> roleToAccessibleCalculatedMembers =
058 new SoftSmartCache<Role, List<Member>>();
059
060 /**
061 * List of named sets.
062 */
063 private Formula[] namedSets;
064
065 /** Contains {@link HierarchyUsage}s for this cube */
066 private final List<HierarchyUsage> hierarchyUsages;
067
068 private RolapStar star;
069 private ExplicitRules.Group aggGroup;
070
071 /**
072 * True if the cube is being created while loading the schema
073 */
074 private boolean load;
075
076 private final Map<Hierarchy, HierarchyUsage> firstUsageMap =
077 new HashMap<Hierarchy, HierarchyUsage>();
078
079 /**
080 * Refers {@link RolapCubeUsages} if this is a virtual cube
081 */
082 private RolapCubeUsages cubeUsages;
083
084 /**
085 * Private constructor used by both normal cubes and virtual cubes.
086 *
087 * @param schema Schema cube belongs to
088 * @param name Name of cube
089 * @param caption Caption
090 * @param fact Definition of fact table
091 */
092 private RolapCube(
093 RolapSchema schema,
094 MondrianDef.Schema xmlSchema,
095 String name,
096 String caption,
097 boolean isCache,
098 MondrianDef.Relation fact,
099 MondrianDef.CubeDimension[] dimensions,
100 boolean load)
101 {
102 super(name, new RolapDimension[dimensions.length + 1]);
103
104 this.schema = schema;
105 this.caption = caption;
106 this.fact = fact;
107 this.hierarchyUsages = new ArrayList<HierarchyUsage>();
108 this.calculatedMembers = new Formula[0];
109 this.namedSets = new Formula[0];
110 this.load = load;
111
112 if (! isVirtual()) {
113 this.star = schema.getRolapStarRegistry().getOrCreateStar(fact);
114 // only set if different from default (so that if two cubes share
115 // the same fact table, either can turn off caching and both are
116 // effected).
117 if (! isCache) {
118 star.setCacheAggregations(isCache);
119 }
120 }
121
122 if (getLogger().isDebugEnabled()) {
123 if (isVirtual()) {
124 getLogger().debug("RolapCube<init>: virtual cube=" +this.name);
125 } else {
126 getLogger().debug("RolapCube<init>: cube=" +this.name);
127 }
128 }
129
130 RolapDimension measuresDimension = new RolapDimension(
131 schema,
132 Dimension.MEASURES_NAME,
133 DimensionType.MeasuresDimension);
134
135 this.dimensions[0] = measuresDimension;
136
137 this.measuresHierarchy = measuresDimension.newHierarchy(null, false);
138
139 if (!Util.isEmpty(xmlSchema.measuresCaption)) {
140 measuresDimension.setCaption(xmlSchema.measuresCaption);
141 this.measuresHierarchy.setCaption(xmlSchema.measuresCaption);
142 }
143
144 for (int i = 0; i < dimensions.length; i++) {
145 MondrianDef.CubeDimension xmlCubeDimension = dimensions[i];
146 // Look up usages of shared dimensions in the schema before
147 // consulting the XML schema (which may be null).
148 RolapCubeDimension dimension =
149 getOrCreateDimension(xmlCubeDimension, schema, xmlSchema, i + 1);
150 if (getLogger().isDebugEnabled()) {
151 getLogger().debug("RolapCube<init>: dimension="
152 + dimension.getName());
153 }
154 this.dimensions[i + 1] = dimension;
155
156 if (! isVirtual()) {
157 createUsages(dimension, xmlCubeDimension);
158 }
159
160 // the register Dimension call was moved here
161 // to keep the RolapStar in sync with the realiasing
162 // within the RolapCubeHierarchy objects.
163 registerDimension(dimension);
164 }
165
166 schema.addCube(this);
167 }
168
169 /**
170 * Creates a <code>RolapCube</code> from a regular cube.
171 */
172 RolapCube(
173 RolapSchema schema,
174 MondrianDef.Schema xmlSchema,
175 MondrianDef.Cube xmlCube,
176 boolean load)
177 {
178 this(
179 schema, xmlSchema, xmlCube.name, xmlCube.caption, xmlCube.cache,
180 xmlCube.fact, xmlCube.dimensions, load);
181
182 if (fact == null) {
183 throw Util.newError(
184 "Must specify fact table of cube '" +
185 getName() + "'");
186 }
187
188 if (fact.getAlias() == null) {
189 throw Util.newError(
190 "Must specify alias for fact table of cube '" +
191 getName() + "'");
192 }
193
194 // since MondrianDef.Measure and MondrianDef.VirtualCubeMeasure
195 // can not be treated as the same, measure creation can not be
196 // done in a common constructor.
197 RolapLevel measuresLevel = this.measuresHierarchy.newMeasuresLevel();
198
199 List<RolapMember> measureList =
200 new ArrayList<RolapMember>(xmlCube.measures.length);
201 Member defaultMeasure = null;
202 for (int i = 0; i < xmlCube.measures.length; i++) {
203 MondrianDef.Measure xmlMeasure = xmlCube.measures[i];
204 MondrianDef.Expression measureExp;
205 if (xmlMeasure.column != null) {
206 if (xmlMeasure.measureExp != null) {
207 throw MondrianResource.instance().
208 BadMeasureSource.ex(
209 xmlCube.name, xmlMeasure.name);
210 }
211 measureExp = new MondrianDef.Column(
212 fact.getAlias(), xmlMeasure.column);
213 } else if (xmlMeasure.measureExp != null) {
214 measureExp = xmlMeasure.measureExp;
215 } else {
216 throw MondrianResource.instance().
217 BadMeasureSource.ex(
218 xmlCube.name, xmlMeasure.name);
219 }
220
221 // Validate aggregator name. Substitute deprecated "distinct count"
222 // with modern "distinct-count".
223 String aggregator = xmlMeasure.aggregator;
224 if (aggregator.equals("distinct count")) {
225 aggregator = RolapAggregator.DistinctCount.getName();
226 }
227 final RolapBaseCubeMeasure measure = new RolapBaseCubeMeasure(
228 this, null, measuresLevel, xmlMeasure.name,
229 xmlMeasure.formatString, measureExp,
230 aggregator, xmlMeasure.datatype);
231 measureList.add(measure);
232 if(Util.equalName(measure.getName(),xmlCube.defaultMeasure)){
233 defaultMeasure = measure;
234 }
235
236 try {
237 CellFormatter cellFormatter =
238 getCellFormatter(xmlMeasure.formatter);
239 if (cellFormatter != null) {
240 measure.setFormatter(cellFormatter);
241 }
242 } catch (Exception e) {
243 throw MondrianResource.instance().CellFormatterLoadFailed.ex(
244 xmlMeasure.formatter, measure.getUniqueName(), e);
245 }
246
247 // Set member's caption, if present.
248 if (!Util.isEmpty(xmlMeasure.caption)) {
249 // there is a special caption string
250 measure.setProperty(
251 Property.CAPTION.name,
252 xmlMeasure.caption);
253 }
254
255 // Set member's visibility, default true.
256 Boolean visible = xmlMeasure.visible;
257 if (visible == null) {
258 visible = Boolean.TRUE;
259 }
260 measure.setProperty(Property.VISIBLE.name, visible);
261
262 List<String> propNames = new ArrayList<String>();
263 List<String> propExprs = new ArrayList<String>();
264 validateMemberProps(
265 xmlMeasure.memberProperties, propNames, propExprs,
266 xmlMeasure.name);
267 int ordinal = i;
268 for (int j = 0; j < propNames.size(); j++) {
269 String propName = propNames.get(j);
270 final Object propExpr = propExprs.get(j);
271 measure.setProperty(propName, propExpr);
272 if (propName.equals(Property.MEMBER_ORDINAL.name)
273 && propExpr instanceof String) {
274 final String expr = (String) propExpr;
275 if (expr.startsWith("\"")
276 && expr.endsWith("\"")) {
277 try {
278 ordinal =
279 Integer.valueOf(
280 expr.substring(1, expr.length() - 1));
281 } catch (NumberFormatException e) {
282 Util.discard(e);
283 }
284 }
285 }
286 }
287 measure.setOrdinal(ordinal);
288 }
289
290 setMeasuresHierarchyMemberReader(
291 new CacheMemberReader(
292 new MeasureMemberSource(this.measuresHierarchy, measureList)));
293
294 this.measuresHierarchy.setDefaultMember(defaultMeasure);
295 init(xmlCube.dimensions);
296 init(xmlCube, measureList);
297
298 setMeasuresHierarchyMemberReader(
299 new CacheMemberReader(
300 new MeasureMemberSource(this.measuresHierarchy, measureList)));
301
302 checkOrdinals(xmlCube.name, measureList);
303 loadAggGroup(xmlCube);
304 }
305
306 /**
307 * this method makes sure that the schemaReader cache is invalidated.
308 * problems can occur if the measure hierarchy member reader is out
309 * of sync with the cache.
310 *
311 * @param memberReader new member reader for measures hierarchy
312 */
313 private void setMeasuresHierarchyMemberReader(MemberReader memberReader) {
314 this.measuresHierarchy.setMemberReader(memberReader);
315 // this invalidates any cached schema reader
316 this.schemaReader = null;
317 }
318
319 /**
320 * Given the name of a cell formatter class, returns a cell formatter.
321 * If class name is null, returns null.
322 *
323 * @param cellFormatterClassName Name of cell formatter class
324 * @return Cell formatter or null
325 * @throws Exception if class cannot be instantiated
326 */
327 static CellFormatter getCellFormatter(
328 String cellFormatterClassName)
329 throws Exception
330 {
331 if (Util.isEmpty(cellFormatterClassName)) {
332 return null;
333 }
334 //noinspection unchecked
335 Class<CellFormatter> clazz =
336 (Class<CellFormatter>)
337 Class.forName(cellFormatterClassName);
338 Constructor<CellFormatter> ctor = clazz.getConstructor();
339 return ctor.newInstance();
340 }
341
342 /**
343 * Creates a <code>RolapCube</code> from a virtual cube.
344 */
345 RolapCube(
346 RolapSchema schema,
347 MondrianDef.Schema xmlSchema,
348 MondrianDef.VirtualCube xmlVirtualCube,
349 boolean load)
350 {
351 this(schema, xmlSchema, xmlVirtualCube.name, xmlVirtualCube.caption,
352 true, null, xmlVirtualCube.dimensions, load);
353
354
355 // Since MondrianDef.Measure and MondrianDef.VirtualCubeMeasure cannot
356 // be treated as the same, measure creation cannot be done in a common
357 // constructor.
358 RolapLevel measuresLevel = this.measuresHierarchy.newMeasuresLevel();
359
360 // Recreate CalculatedMembers, as the original members point to
361 // incorrect dimensional ordinals for the virtual cube.
362 List<RolapVirtualCubeMeasure> origMeasureList =
363 new ArrayList<RolapVirtualCubeMeasure>();
364 List<MondrianDef.CalculatedMember> origCalcMeasureList =
365 new ArrayList<MondrianDef.CalculatedMember>();
366 CubeComparator cubeComparator = new CubeComparator();
367 Map<RolapCube, List<MondrianDef.CalculatedMember>> calculatedMembersMap =
368 new TreeMap<RolapCube, List<MondrianDef.CalculatedMember>>(
369 cubeComparator);
370 Member defaultMeasure = null;
371
372 this.cubeUsages = new RolapCubeUsages(xmlVirtualCube.cubeUsage);
373
374 for (MondrianDef.VirtualCubeMeasure xmlMeasure : xmlVirtualCube.measures) {
375 // Lookup a measure in an existing cube.
376 RolapCube cube = schema.lookupCube(xmlMeasure.cubeName);
377 Member[] cubeMeasures = cube.getMeasures();
378 boolean found = false;
379 for (Member cubeMeasure : cubeMeasures) {
380 if (cubeMeasure.getUniqueName().equals(xmlMeasure.name)) {
381 if (cubeMeasure.getName().equalsIgnoreCase(xmlVirtualCube.defaultMeasure)){
382 defaultMeasure = cubeMeasure;
383 }
384 found = true;
385 if (cubeMeasure instanceof RolapCalculatedMember) {
386 // We have a calulated member! Keep track of which
387 // base cube each calculated member is associated
388 // with, so we can resolve the calculated member
389 // relative to its base cube. We're using a treeMap
390 // to store the mapping to ensure a deterministic
391 // order for the members.
392 MondrianDef.CalculatedMember calcMember =
393 schema.lookupXmlCalculatedMember(
394 xmlMeasure.name, xmlMeasure.cubeName);
395 if (calcMember == null) {
396 throw Util.newInternal(
397 "Could not find XML Calculated Member '" +
398 xmlMeasure.name + "' in XML cube '" +
399 xmlMeasure.cubeName + "'");
400 }
401 List<MondrianDef.CalculatedMember> memberList =
402 calculatedMembersMap.get(cube);
403 if (memberList == null) {
404 memberList =
405 new ArrayList<MondrianDef.CalculatedMember>();
406 }
407 memberList.add(calcMember);
408 origCalcMeasureList.add(calcMember);
409 calculatedMembersMap.put(cube, memberList);
410 } else {
411 // This is the a standard measure. (Don't know
412 // whether it will confuse things that this
413 // measure still points to its 'real' cube.)
414 RolapVirtualCubeMeasure virtualCubeMeasure =
415 new RolapVirtualCubeMeasure(
416 null,
417 measuresLevel,
418 (RolapStoredMeasure) cubeMeasure);
419
420 // Set member's visibility, default true.
421 Boolean visible = xmlMeasure.visible;
422 if (visible == null) {
423 visible = Boolean.TRUE;
424 }
425 virtualCubeMeasure.setProperty(Property.VISIBLE.name,
426 visible);
427 // Inherit caption from the "real" measure
428 virtualCubeMeasure.setProperty(Property.CAPTION.name,
429 cubeMeasure.getCaption());
430 origMeasureList.add(virtualCubeMeasure);
431 }
432 break;
433 }
434 }
435 if (!found) {
436 throw Util.newInternal(
437 "could not find measure '" + xmlMeasure.name +
438 "' in cube '" + xmlMeasure.cubeName + "'");
439 }
440 }
441
442 // Must init the dimensions before dealing with calculated members
443 init(xmlVirtualCube.dimensions);
444
445 // Loop through the base cubes containing calculated members
446 // referenced by this virtual cube. Resolve those members relative
447 // to their base cubes first, then resolve them relative to this
448 // cube so the correct dimension ordinals are used
449 List<RolapVirtualCubeMeasure> modifiedMeasureList =
450 new ArrayList<RolapVirtualCubeMeasure>(origMeasureList);
451 for (Object o : calculatedMembersMap.keySet()) {
452 RolapCube baseCube = (RolapCube) o;
453 List<MondrianDef.CalculatedMember> calculatedMemberList =
454 calculatedMembersMap.get(baseCube);
455 Query queryExp = resolveCalcMembers(
456 calculatedMemberList.toArray(
457 new MondrianDef.CalculatedMember[
458 calculatedMemberList.size()]),
459 new MondrianDef.NamedSet[0],
460 baseCube,
461 false);
462 MeasureFinder measureFinder =
463 new MeasureFinder(this, baseCube, measuresLevel);
464 queryExp.accept(measureFinder);
465 modifiedMeasureList.addAll(measureFinder.getMeasuresFound());
466 }
467
468 // Add the original calculated members from the base cubes to our
469 // list of calculated members
470 List<MondrianDef.CalculatedMember> calculatedMemberList =
471 new ArrayList<MondrianDef.CalculatedMember>();
472 for (Object o : calculatedMembersMap.keySet()) {
473 RolapCube baseCube = (RolapCube) o;
474 calculatedMemberList.addAll(
475 calculatedMembersMap.get(baseCube));
476 }
477 calculatedMemberList.addAll(
478 Arrays.asList(xmlVirtualCube.calculatedMembers));
479
480
481 // Resolve all calculated members relative to this virtual cube,
482 // whose measureHierarchy member reader now contains all base
483 // measures referenced in those calculated members
484 setMeasuresHierarchyMemberReader(
485 new CacheMemberReader(
486 new MeasureMemberSource(
487 this.measuresHierarchy,
488 Util.<RolapMember>cast(modifiedMeasureList))));
489
490 createCalcMembersAndNamedSets(
491 calculatedMemberList.toArray(
492 new MondrianDef.CalculatedMember[
493 calculatedMemberList.size()]),
494 xmlVirtualCube.namedSets,
495 new ArrayList<RolapMember>(),
496 new ArrayList<Formula>(),
497 this,
498 false);
499
500 // reset the measureHierarchy member reader back to the list of
501 // measures that are only defined on this virtual cube
502 setMeasuresHierarchyMemberReader(
503 new CacheMemberReader(
504 new MeasureMemberSource(
505 this.measuresHierarchy,
506 Util.<RolapMember>cast(origMeasureList))));
507
508 this.measuresHierarchy.setDefaultMember(defaultMeasure);
509
510
511 // remove from the calculated members array those members that weren't
512 // originally defined on this virtual cube
513 List<Formula> finalCalcMemberList = new ArrayList<Formula>();
514 for (Formula calculatedMember : calculatedMembers) {
515 if (findOriginalMembers(
516 calculatedMember,
517 origCalcMeasureList,
518 finalCalcMemberList)) {
519 continue;
520 }
521 findOriginalMembers(
522 calculatedMember,
523 Arrays.asList(xmlVirtualCube.calculatedMembers),
524 finalCalcMemberList);
525 }
526 calculatedMembers =
527 finalCalcMemberList.toArray(
528 new Formula[finalCalcMemberList.size()]);
529
530 for (Formula calcMember : finalCalcMemberList) {
531 if (calcMember.getName().
532 equalsIgnoreCase(xmlVirtualCube.defaultMeasure)){
533 this.measuresHierarchy.setDefaultMember(calcMember.getMdxMember());
534 break;
535 }
536 }
537
538 // Note: virtual cubes do not get aggregate
539 }
540
541 private boolean findOriginalMembers(
542 Formula formula,
543 List<MondrianDef.CalculatedMember> calcMemberList,
544 List<Formula> finalCalcMemberList)
545 {
546 for (MondrianDef.CalculatedMember xmlCalcMember : calcMemberList) {
547 Dimension dimension =
548 (Dimension) lookupDimension(
549 new Id.Segment(
550 xmlCalcMember.dimension,
551 Id.Quoting.UNQUOTED));
552 if (formula.getName().equals(xmlCalcMember.name) &&
553 formula.getMdxMember().getDimension().getName().equals(
554 dimension.getName())) {
555 finalCalcMemberList.add(formula);
556 return true;
557 }
558 }
559 return false;
560 }
561
562 protected Logger getLogger() {
563 return LOGGER;
564 }
565
566 public boolean hasAggGroup() {
567 return (aggGroup != null);
568 }
569 public ExplicitRules.Group getAggGroup() {
570 return aggGroup;
571 }
572 void loadAggGroup(MondrianDef.Cube xmlCube) {
573 aggGroup = ExplicitRules.Group.make(this, xmlCube);
574 }
575
576 /**
577 * Creates a dimension from its XML definition. If the XML definition is
578 * a <DimensionUsage>, and the shared dimension is cached in the
579 * schema, returns that.
580 *
581 * @param xmlCubeDimension XML Dimension or DimensionUsage
582 * @param schema Schema
583 * @param xmlSchema XML Schema
584 * @param dimensionOrdinal Ordinal of dimension
585 * @return A dimension
586 */
587 private RolapCubeDimension getOrCreateDimension(
588 MondrianDef.CubeDimension xmlCubeDimension,
589 RolapSchema schema,
590 MondrianDef.Schema xmlSchema,
591 int dimensionOrdinal)
592 {
593 RolapDimension dimension = null;
594 if (xmlCubeDimension instanceof MondrianDef.DimensionUsage) {
595 MondrianDef.DimensionUsage usage =
596 (MondrianDef.DimensionUsage) xmlCubeDimension;
597 final RolapHierarchy sharedHierarchy =
598 schema.getSharedHierarchy(usage.source);
599 if (sharedHierarchy != null) {
600 dimension =
601 (RolapDimension) sharedHierarchy.getDimension();
602 }
603 }
604
605 if (dimension == null) {
606 MondrianDef.Dimension xmlDimension =
607 xmlCubeDimension.getDimension(xmlSchema);
608 dimension =
609 new RolapDimension(
610 schema, this, xmlDimension, xmlCubeDimension);
611 }
612
613 // wrap the shared or regular dimension with a
614 // rolap cube dimension object
615 return new RolapCubeDimension(
616 this, dimension, xmlCubeDimension,
617 xmlCubeDimension.name, dimensionOrdinal);
618 }
619
620 /**
621 * Post-initialization, doing things which cannot be done in the
622 * constructor.
623 */
624 private void init(
625 MondrianDef.Cube xmlCube,
626 final List<RolapMember> memberList)
627 {
628 // Load calculated members and named sets.
629 // (We cannot do this in the constructor, because
630 // cannot parse the generated query, because the schema has not been
631 // set in the cube at this point.)
632 List<Formula> formulaList = new ArrayList<Formula>();
633 createCalcMembersAndNamedSets(
634 xmlCube.calculatedMembers, xmlCube.namedSets,
635 memberList, formulaList, this, true);
636 }
637
638 /**
639 * Checks that the ordinals of measures (including calculated measures)
640 * are unique.
641 *
642 * @param cubeName name of the cube (required for error messages)
643 * @param measures measure list
644 */
645 private void checkOrdinals(
646 String cubeName,
647 List<RolapMember> measures)
648 {
649 Map<Integer, String> ordinals = new HashMap<Integer, String>();
650 for (RolapMember measure : measures) {
651 Integer ordinal = measure.getOrdinal();
652 if (!ordinals.containsKey(ordinal)) {
653 ordinals.put(ordinal, measure.getUniqueName());
654 } else {
655 throw MondrianResource.instance().MeasureOrdinalsNotUnique.ex(
656 cubeName,
657 ordinal.toString(),
658 ordinals.get(ordinal),
659 measure.getUniqueName());
660 }
661 }
662 }
663
664 /**
665 * Adds a collection of calculated members and named sets to this cube.
666 * The members and sets can refer to each other.
667 *
668 * @param xmlCalcMembers XML objects representing members
669 * @param xmlNamedSets Array of XML definition of named set
670 * @param memberList Output list of {@link Member} objects
671 * @param formulaList Output list of {@link Formula} objects
672 * @param cube the cube that the calculated members originate from
673 * @param errOnDups throws an error if a duplicate member is found
674 */
675 private void createCalcMembersAndNamedSets(
676 MondrianDef.CalculatedMember[] xmlCalcMembers,
677 MondrianDef.NamedSet[] xmlNamedSets,
678 List<RolapMember> memberList,
679 List<Formula> formulaList,
680 RolapCube cube,
681 boolean errOnDups) {
682
683 final Query queryExp =
684 resolveCalcMembers(
685 xmlCalcMembers,
686 xmlNamedSets,
687 cube,
688 errOnDups);
689 if (queryExp == null) {
690 return;
691 }
692
693 // Now pick through the formulas.
694 Util.assertTrue(queryExp.formulas.length ==
695 xmlCalcMembers.length + xmlNamedSets.length);
696 for (int i = 0; i < xmlCalcMembers.length; i++) {
697 postCalcMember(xmlCalcMembers, i, queryExp, memberList);
698 }
699 for (int i = 0; i < xmlNamedSets.length; i++) {
700 postNamedSet(xmlNamedSets, xmlCalcMembers.length, i, queryExp, formulaList);
701 }
702 }
703
704 private Query resolveCalcMembers(
705 MondrianDef.CalculatedMember[] xmlCalcMembers,
706 MondrianDef.NamedSet[] xmlNamedSets,
707 RolapCube cube,
708 boolean errOnDups)
709 {
710 // If there are no objects to create, our generated SQL will be so
711 // silly, the parser will laugh.
712 if (xmlCalcMembers.length == 0 && xmlNamedSets.length == 0) {
713 return null;
714 }
715
716 StringBuilder buf = new StringBuilder(256);
717 buf.append("WITH").append(Util.nl);
718
719 // Check the members individually, and generate SQL.
720 for (int i = 0; i < xmlCalcMembers.length; i++) {
721 preCalcMember(xmlCalcMembers, i, buf, cube, errOnDups);
722 }
723
724 // Check the named sets individually (for uniqueness) and generate SQL.
725 Set<String> nameSet = new HashSet<String>();
726 for (Formula namedSet : namedSets) {
727 nameSet.add(namedSet.getName());
728 }
729 for (MondrianDef.NamedSet xmlNamedSet : xmlNamedSets) {
730 preNamedSet(xmlNamedSet, nameSet, buf);
731 }
732
733 buf.append("SELECT FROM ").append(cube.getUniqueName());
734
735 // Parse and validate this huge MDX query we've created.
736 final String queryString = buf.toString();
737 final Query queryExp;
738 try {
739 RolapConnection conn = schema.getInternalConnection();
740 queryExp = conn.parseQuery(queryString, load);
741 } catch (Exception e) {
742 throw MondrianResource.instance().UnknownNamedSetHasBadFormula.ex(
743 getName(), e);
744 }
745 queryExp.resolve();
746 return queryExp;
747 }
748
749 private void postNamedSet(
750 MondrianDef.NamedSet[] xmlNamedSets,
751 final int offset, int i,
752 final Query queryExp,
753 List<Formula> formulaList) {
754 MondrianDef.NamedSet xmlNamedSet = xmlNamedSets[i];
755 Util.discard(xmlNamedSet);
756 Formula formula = queryExp.formulas[offset + i];
757 namedSets = RolapUtil.addElement(namedSets, formula);
758 formulaList.add(formula);
759 }
760
761 private void preNamedSet(
762 MondrianDef.NamedSet xmlNamedSet,
763 Set<String> nameSet,
764 StringBuilder buf) {
765 if (!nameSet.add(xmlNamedSet.name)) {
766 throw MondrianResource.instance().NamedSetNotUnique.ex(
767 xmlNamedSet.name, getName());
768 }
769
770 buf.append("SET ")
771 .append(Util.makeFqName(xmlNamedSet.name))
772 .append(Util.nl)
773 .append(" AS ");
774 Util.singleQuoteString(xmlNamedSet.getFormula(), buf);
775 buf.append(Util.nl);
776 }
777
778 private void postCalcMember(
779 MondrianDef.CalculatedMember[] xmlCalcMembers,
780 int i,
781 final Query queryExp,
782 List<RolapMember> memberList)
783 {
784 MondrianDef.CalculatedMember xmlCalcMember = xmlCalcMembers[i];
785 final Formula formula = queryExp.formulas[i];
786
787 calculatedMembers = RolapUtil.addElement(calculatedMembers, formula);
788
789 Member member = formula.getMdxMember();
790
791 Boolean visible = xmlCalcMember.visible;
792 if (visible == null) {
793 visible = Boolean.TRUE;
794 }
795 member.setProperty(Property.VISIBLE.name, visible);
796
797 if ((xmlCalcMember.caption != null) &&
798 xmlCalcMember.caption.length() > 0) {
799 member.setProperty(
800 Property.CAPTION.name,
801 xmlCalcMember.caption);
802 }
803
804 memberList.add((RolapMember) formula.getMdxMember());
805 }
806
807 private void preCalcMember(
808 MondrianDef.CalculatedMember[] xmlCalcMembers,
809 int j,
810 StringBuilder buf,
811 RolapCube cube,
812 boolean errOnDup) {
813 MondrianDef.CalculatedMember xmlCalcMember = xmlCalcMembers[j];
814
815 // Lookup dimension
816 final Dimension dimension =
817 (Dimension) lookupDimension(
818 new Id.Segment(
819 xmlCalcMember.dimension,
820 Id.Quoting.UNQUOTED));
821 if (dimension == null) {
822 throw MondrianResource.instance().CalcMemberHasBadDimension.ex(
823 xmlCalcMember.dimension, xmlCalcMember.name, getName());
824 }
825
826 // If we're processing a virtual cube, it's possible that we've
827 // already processed this calculated member because it's
828 // referenced in another measure; in that case, remove it from the
829 // list, since we'll add it back in later; otherwise, in the
830 // non-virtual cube case, throw an exception
831 List<Formula> newCalcMemberList = new ArrayList<Formula>();
832 for (Formula formula : calculatedMembers) {
833 if (formula.getName().equals(xmlCalcMember.name) &&
834 formula.getMdxMember().getDimension().getName().equals(
835 dimension.getName())) {
836 if (errOnDup) {
837 throw MondrianResource.instance().CalcMemberNotUnique.ex(
838 Util.makeFqName(dimension, xmlCalcMember.name),
839 getName());
840 }
841 continue;
842 } else {
843 newCalcMemberList.add(formula);
844 }
845 }
846 calculatedMembers =
847 newCalcMemberList.toArray(new Formula[newCalcMemberList.size()]);
848
849 // Check this calc member doesn't clash with one earlier in this
850 // batch.
851 for (int k = 0; k < j; k++) {
852 MondrianDef.CalculatedMember xmlCalcMember2 = xmlCalcMembers[k];
853 if (xmlCalcMember2.name.equals(xmlCalcMember.name) &&
854 xmlCalcMember2.dimension.equals(xmlCalcMember.dimension)) {
855 throw MondrianResource.instance().CalcMemberNotUnique.ex(
856 Util.makeFqName(dimension, xmlCalcMember.name),
857 getName());
858 }
859 }
860
861 final String memberUniqueName = Util.makeFqName(
862 dimension.getUniqueName(), xmlCalcMember.name);
863 final MondrianDef.CalculatedMemberProperty[] xmlProperties =
864 xmlCalcMember.memberProperties;
865 List<String> propNames = new ArrayList<String>();
866 List<String> propExprs = new ArrayList<String>();
867 validateMemberProps(
868 xmlProperties, propNames, propExprs, xmlCalcMember.name);
869
870 final int measureCount =
871 cube.measuresHierarchy.getMemberReader().getMemberCount();
872
873 // Generate SQL.
874 assert memberUniqueName.startsWith("[");
875 buf.append("MEMBER ").append(memberUniqueName)
876 .append(Util.nl)
877 .append(" AS ");
878 Util.singleQuoteString(xmlCalcMember.getFormula(), buf);
879
880 assert propNames.size() == propExprs.size();
881 processFormatStringAttribute(xmlCalcMember, buf);
882
883 for (int i = 0; i < propNames.size(); i++) {
884 String name = propNames.get(i);
885 String expr = propExprs.get(i);
886 buf.append(",").append(Util.nl);
887 expr = removeSurroundingQuotesIfNumericProperty(name, expr);
888 buf.append(name).append(" = ").append(expr);
889 }
890 // Flag that the calc members are defined against a cube; will
891 // determine the value of Member.isCalculatedInQuery
892 buf.append(",").append(Util.nl).
893 append(Util.quoteMdxIdentifier(Property.MEMBER_SCOPE.name)).
894 append(" = 'CUBE'");
895
896 // Assign the member an ordinal higher than all of the stored measures.
897 if (!propNames.contains(Property.MEMBER_ORDINAL.getName())) {
898 buf.append(",").append(Util.nl).
899 append(Property.MEMBER_ORDINAL).append(" = ").
900 append(measureCount + j);
901 }
902 buf.append(Util.nl);
903 }
904
905 private String removeSurroundingQuotesIfNumericProperty(String name, String expr) {
906 Property prop = Property.lookup(name, false);
907 if (prop != null && prop.getType() == Property.Datatype.TYPE_NUMERIC &&
908 isSurroundedWithQuotes(expr) && expr.length() > 2) {
909 return expr.substring(1, expr.length() - 1);
910 }
911 return expr;
912 }
913
914 private boolean isSurroundedWithQuotes(String expr) {
915 return expr.startsWith("\"") && expr.endsWith("\"");
916 }
917
918 void processFormatStringAttribute(MondrianDef.CalculatedMember xmlCalcMember, StringBuilder buf) {
919 if (xmlCalcMember.formatString != null) {
920 buf.append(",").append(Util.nl)
921 .append(Property.FORMAT_STRING.name).append(" = ").append(Util.quoteForMdx(xmlCalcMember.formatString));
922 }
923 }
924
925 /**
926 * Validates an array of member properties, and populates a list of names
927 * and expressions, one for each property.
928 *
929 * @param xmlProperties Array of property definitions.
930 * @param propNames Output array of property names.
931 * @param propExprs Output array of property expressions.
932 * @param memberName Name of member which the properties belong to.
933 */
934 private void validateMemberProps(
935 final MondrianDef.CalculatedMemberProperty[] xmlProperties,
936 List<String> propNames,
937 List<String> propExprs,
938 String memberName) {
939
940 MemberProperty[] properties = new MemberProperty[xmlProperties.length];
941 for (int i = 0; i < properties.length; i++) {
942 final MondrianDef.CalculatedMemberProperty xmlProperty =
943 xmlProperties[i];
944 if (xmlProperty.expression == null &&
945 xmlProperty.value == null)
946 {
947 throw MondrianResource.instance()
948 .NeitherExprNorValueForCalcMemberProperty.ex(
949 xmlProperty.name,
950 memberName,
951 getName());
952 }
953 if (xmlProperty.expression != null &&
954 xmlProperty.value != null)
955 {
956 throw MondrianResource.instance()
957 .ExprAndValueForMemberProperty.ex(
958 xmlProperty.name,
959 memberName,
960 getName());
961 }
962 propNames.add(xmlProperty.name);
963 if (xmlProperty.expression != null) {
964 propExprs.add(xmlProperty.expression);
965 } else {
966 propExprs.add(Util.quoteForMdx(xmlProperty.value));
967 }
968 }
969 }
970
971 public RolapSchema getSchema() {
972 return schema;
973 }
974
975 /**
976 * Returns the named sets of this cube.
977 */
978 public NamedSet[] getNamedSets() {
979 NamedSet[] namedSetsArray = new NamedSet[namedSets.length];
980 for (int i=0; i < namedSets.length; i++) {
981 namedSetsArray[i] = namedSets[i].getNamedSet();
982 }
983 return namedSetsArray;
984 }
985
986 /**
987 * Returns the schema reader which enforces the appropriate access-control
988 * context. schemaReader is cached, and needs to stay in sync with
989 * any changes to the cube.
990 *
991 * @post return != null
992 * @see #getSchemaReader(Role)
993 */
994 public synchronized SchemaReader getSchemaReader() {
995 if (schemaReader == null) {
996 RoleImpl schemaDefaultRoleImpl = schema.getDefaultRole();
997 RoleImpl roleImpl = schemaDefaultRoleImpl.makeMutableClone();
998 roleImpl.grant(this, Access.ALL);
999 Role role = roleImpl;
1000 schemaReader = new RolapCubeSchemaReader(role);
1001 }
1002 return schemaReader;
1003 }
1004
1005 public SchemaReader getSchemaReader(Role role) {
1006 if (role == null) {
1007 return getSchemaReader();
1008 } else {
1009 return new RolapCubeSchemaReader(role);
1010 }
1011 }
1012
1013 MondrianDef.CubeDimension lookup(
1014 MondrianDef.CubeDimension[] xmlDimensions,
1015 String name) {
1016 for (MondrianDef.CubeDimension cd : xmlDimensions) {
1017 if (name.equals(cd.name)) {
1018 return cd;
1019 }
1020 }
1021 // TODO: this ought to be a fatal error.
1022 return null;
1023 }
1024
1025 private void init(MondrianDef.CubeDimension[] xmlDimensions) {
1026 for (Dimension dimension1 : dimensions) {
1027 final RolapDimension dimension = (RolapDimension) dimension1;
1028 dimension.init(lookup(xmlDimensions, dimension.getName()));
1029 }
1030 register();
1031 }
1032
1033 private void register() {
1034 if (isVirtual()) {
1035 return;
1036 }
1037 List<Member> list = new ArrayList<Member>();
1038 Member[] measures = getMeasures();
1039 for (Member measure : measures) {
1040 if (measure instanceof RolapBaseCubeMeasure) {
1041 list.add(measure);
1042 }
1043 }
1044 RolapBaseCubeMeasure[] storedMeasures =
1045 list.toArray(new RolapBaseCubeMeasure[list.size()]);
1046
1047 RolapStar star = getStar();
1048 RolapStar.Table table = star.getFactTable();
1049
1050 // create measures (and stars for them, if necessary)
1051 for (RolapBaseCubeMeasure storedMeasure : storedMeasures) {
1052 table.makeMeasure(storedMeasure);
1053 }
1054 }
1055
1056 /**
1057 * Returns true if this Cube is either virtual or if the Cube's
1058 * RolapStar is caching aggregates.
1059 *
1060 * @return Whether this Cube's RolapStar should cache aggregations
1061 */
1062 public boolean isCacheAggregations() {
1063 return isVirtual() || star.isCacheAggregations();
1064 }
1065
1066 /**
1067 * Set if this (non-virtual) Cube's RolapStar should cache
1068 * aggregations.
1069 *
1070 * @param cache Whether this Cube's RolapStar should cache aggregations
1071 */
1072 public void setCacheAggregations(boolean cache) {
1073 if (! isVirtual()) {
1074 star.setCacheAggregations(cache);
1075 }
1076 }
1077
1078 /**
1079 * Clear the in memory aggregate cache associated with this Cube, but
1080 * only if Disabling Caching has been enabled.
1081 */
1082 public void clearCachedAggregations() {
1083 clearCachedAggregations(false);
1084 }
1085
1086 /**
1087 * Clear the in memory aggregate cache associated with this Cube.
1088 */
1089 public void clearCachedAggregations(boolean forced) {
1090 if (isVirtual()) {
1091 // TODO:
1092 // Currently a virtual cube does not keep a list of all of its
1093 // base cubes, so we need to iterate through each and flush
1094 // the ones that should be flushed. Could use a CacheControl
1095 // method here.
1096 for (RolapStar star1 : schema.getStars()) {
1097 // this will only flush the star's aggregate cache if
1098 // 1) DisableCaching is true or 2) the star's cube has
1099 // cacheAggregations set to false in the schema.
1100 star1.clearCachedAggregations(forced);
1101 }
1102 } else {
1103 star.clearCachedAggregations(forced);
1104 }
1105 }
1106
1107 /**
1108 * Check if there are modifications in the aggregations cache
1109 */
1110 public void checkAggregateModifications() {
1111 if (isVirtual()) {
1112 // TODO:
1113 // Currently a virtual cube does not keep a list of all of its
1114 // base cubes, so we need to iterate through each and flush
1115 // the ones that should be flushed
1116 schema.checkAggregateModifications();
1117 } else {
1118 star.checkAggregateModifications();
1119 }
1120 }
1121 /**
1122 * Push all modifications of the aggregations to global cache,
1123 * so other queries can start using the new cache
1124 */
1125 public void pushAggregateModificationsToGlobalCache() {
1126 if (isVirtual()) {
1127 // TODO:
1128 // Currently a virtual cube does not keep a list of all of its
1129 // base cubes, so we need to iterate through each and flush
1130 // the ones that should be flushed
1131 schema.pushAggregateModificationsToGlobalCache();
1132 } else {
1133 star.pushAggregateModificationsToGlobalCache();
1134 }
1135 }
1136
1137
1138
1139 /**
1140 * Returns this cube's underlying star schema.
1141 */
1142 public RolapStar getStar() {
1143 return star;
1144 }
1145
1146 private void createUsages(RolapCubeDimension dimension,
1147 MondrianDef.CubeDimension xmlCubeDimension) {
1148 // RME level may not be in all hierarchies
1149 // If one uses the DimensionUsage attribute "level", which level
1150 // in a hierarchy to join on, and there is more than one hierarchy,
1151 // then a HierarchyUsage can not be created for the hierarchies
1152 // that do not have the level defined.
1153 RolapCubeHierarchy[] hierarchies =
1154 (RolapCubeHierarchy[]) dimension.getHierarchies();
1155
1156 if (hierarchies.length == 1) {
1157 // Only one, so let lower level error checking handle problems
1158 createUsage(hierarchies[0], xmlCubeDimension);
1159
1160 } else if ((xmlCubeDimension instanceof MondrianDef.DimensionUsage) &&
1161 (((MondrianDef.DimensionUsage) xmlCubeDimension).level != null)) {
1162 // More than one, make sure if we are joining by level, that
1163 // at least one hierarchy can and those that can not are
1164 // not registered
1165 MondrianDef.DimensionUsage du =
1166 (MondrianDef.DimensionUsage) xmlCubeDimension;
1167
1168 int cnt = 0;
1169
1170 for (RolapCubeHierarchy hierarchy : hierarchies) {
1171 if (getLogger().isDebugEnabled()) {
1172 getLogger().debug("RolapCube<init>: hierarchy="
1173 + hierarchy.getName());
1174 }
1175 RolapLevel joinLevel = (RolapLevel)
1176 Util.lookupHierarchyLevel(hierarchy, du.level);
1177 if (joinLevel == null) {
1178 continue;
1179 }
1180 createUsage(hierarchy, xmlCubeDimension);
1181 cnt++;
1182 }
1183
1184 if (cnt == 0) {
1185 // None of the hierarchies had the level, let lower level
1186 // detect and throw error
1187 createUsage(hierarchies[0], xmlCubeDimension);
1188 }
1189
1190 } else {
1191 // just do it
1192 for (RolapCubeHierarchy hierarchy : hierarchies) {
1193 if (getLogger().isDebugEnabled()) {
1194 getLogger().debug("RolapCube<init>: hierarchy="
1195 + hierarchy.getName());
1196 }
1197 createUsage(hierarchy, xmlCubeDimension);
1198 }
1199 }
1200 }
1201
1202 synchronized void createUsage(
1203 RolapCubeHierarchy hierarchy,
1204 MondrianDef.CubeDimension cubeDim) {
1205
1206 HierarchyUsage usage = new HierarchyUsage(this, hierarchy, cubeDim);
1207 if (LOGGER.isDebugEnabled()) {
1208 LOGGER.debug("RolapCube.createUsage: "+
1209 "cube=" +getName()+
1210 ", hierarchy=" +hierarchy.getName() +
1211 ", usage=" +usage);
1212 }
1213 for (HierarchyUsage hierUsage : hierarchyUsages) {
1214 if (hierUsage.equals(usage)) {
1215 getLogger().warn(
1216 "RolapCube.createUsage: duplicate " + hierUsage);
1217 return;
1218 }
1219 }
1220 if (getLogger().isDebugEnabled()) {
1221 getLogger().debug("RolapCube.createUsage: register " +usage);
1222 }
1223 this.hierarchyUsages.add(usage);
1224 }
1225
1226 private synchronized HierarchyUsage getUsageByName(String name) {
1227 for (HierarchyUsage hierUsage : hierarchyUsages) {
1228 if (hierUsage.getFullName().equals(name)) {
1229 return hierUsage;
1230 }
1231 }
1232 return null;
1233 }
1234
1235 /**
1236 * A Hierarchy may have one or more HierarchyUsages. This method returns
1237 * an array holding the one or more usages associated with a Hierarchy.
1238 * The HierarchyUsages hierarchyName attribute always equals the name
1239 * attribute of the Hierarchy.
1240 *
1241 * @param hierarchy Hierarchy
1242 * @return an HierarchyUsages array with 0 or more members.
1243 */
1244 public synchronized HierarchyUsage[] getUsages(Hierarchy hierarchy) {
1245 String name = hierarchy.getName();
1246 if (getLogger().isDebugEnabled()) {
1247 getLogger().debug("RolapCube.getUsages: name="+name);
1248 }
1249
1250 HierarchyUsage hierUsage = null;
1251 List<HierarchyUsage> list = null;
1252
1253 for (HierarchyUsage hu : hierarchyUsages) {
1254 if (hu.getHierarchyName().equals(name)) {
1255 if (list != null) {
1256 if (getLogger().isDebugEnabled()) {
1257 getLogger().debug("RolapCube.getUsages: "
1258 + "add list HierarchyUsage.name=" + hu.getName());
1259 }
1260 list.add(hu);
1261 } else if (hierUsage == null) {
1262 hierUsage = hu;
1263 } else {
1264 list = new ArrayList<HierarchyUsage>();
1265 if (getLogger().isDebugEnabled()) {
1266 getLogger().debug("RolapCube.getUsages: "
1267 + "add list hierUsage.name="
1268 + hierUsage.getName()
1269 + ", hu.name="
1270 + hu.getName());
1271 }
1272 list.add(hierUsage);
1273 list.add(hu);
1274 hierUsage = null;
1275 }
1276 }
1277 }
1278 if (hierUsage != null) {
1279 return new HierarchyUsage[] { hierUsage };
1280 } else if (list != null) {
1281 if (getLogger().isDebugEnabled()) {
1282 getLogger().debug("RolapCube.getUsages: return list");
1283 }
1284 return list.toArray(new HierarchyUsage[list.size()]);
1285 } else {
1286 return new HierarchyUsage[0];
1287 }
1288 }
1289
1290 synchronized HierarchyUsage getFirstUsage(Hierarchy hier) {
1291 HierarchyUsage hierarchyUsage = firstUsageMap.get(hier);
1292 if (hierarchyUsage == null) {
1293 HierarchyUsage[] hierarchyUsages = getUsages(hier);
1294 if (hierarchyUsages.length != 0) {
1295 hierarchyUsage = hierarchyUsages[0];
1296 firstUsageMap.put(hier, hierarchyUsage);
1297 }
1298 }
1299 return hierarchyUsage;
1300 }
1301
1302 /**
1303 * Looks up all of the HierarchyUsages with the same "source" returning
1304 * an array of HierarchyUsage of length 0 or more.
1305 *
1306 * This method is currently only called if an error occurs in lookupChild(),
1307 * so that more information can be displayed in the error log.
1308 *
1309 * @param source Name of shared dimension
1310 * @return array of HierarchyUsage (HierarchyUsage[]) - never null.
1311 */
1312 private synchronized HierarchyUsage[] getUsagesBySource(String source) {
1313 if (getLogger().isDebugEnabled()) {
1314 getLogger().debug("RolapCube.getUsagesBySource: source="+source);
1315 }
1316
1317 HierarchyUsage hierUsage = null;
1318 List<HierarchyUsage> list = null;
1319
1320 for (HierarchyUsage hu : hierarchyUsages) {
1321 String s = hu.getSource();
1322 if ((s != null) && s.equals(source)) {
1323 if (list != null) {
1324 if (getLogger().isDebugEnabled()) {
1325 getLogger().debug("RolapCube.getUsagesBySource: "
1326 + "add list HierarchyUsage.name="
1327 + hu.getName());
1328 }
1329 list.add(hu);
1330 } else if (hierUsage == null) {
1331 hierUsage = hu;
1332 } else {
1333 list = new ArrayList<HierarchyUsage>();
1334 if (getLogger().isDebugEnabled()) {
1335 getLogger().debug("RolapCube.getUsagesBySource: "
1336 + "add list hierUsage.name="
1337 + hierUsage.getName()
1338 + ", hu.name="
1339 + hu.getName());
1340 }
1341 list.add(hierUsage);
1342 list.add(hu);
1343 hierUsage = null;
1344 }
1345 }
1346 }
1347 if (hierUsage != null) {
1348 return new HierarchyUsage[] { hierUsage };
1349 } else if (list != null) {
1350 if (getLogger().isDebugEnabled()) {
1351 getLogger().debug("RolapCube.getUsagesBySource: return list");
1352 }
1353 return list.toArray(new HierarchyUsage[list.size()]);
1354 } else {
1355 return new HierarchyUsage[0];
1356 }
1357 }
1358
1359
1360 /**
1361 * Understand this and you are no longer a novice.
1362 *
1363 * @param dimension Dimension
1364 */