Java tutorial
/* // This software is subject to the terms of the Eclipse Public License v1.0 // Agreement, available at the following URL: // http://www.eclipse.org/legal/epl-v10.html. // You must accept the terms of that agreement to use this software. // // Copyright (C) 1998-2005 Julian Hyde // Copyright (C) 2005-2013 Pentaho and others // All Rights Reserved. */ package mondrian.olap; import mondrian.calc.*; import mondrian.mdx.*; import mondrian.olap.fun.ParameterFunDef; import mondrian.olap.type.*; import mondrian.resource.MondrianResource; import mondrian.rolap.*; import mondrian.server.*; import mondrian.spi.ProfileHandler; import mondrian.util.ArrayStack; import org.apache.commons.collections.collection.CompositeCollection; import org.olap4j.impl.IdentifierParser; import org.olap4j.mdx.IdentifierSegment; import java.io.PrintWriter; import java.sql.SQLException; import java.util.*; /** * <code>Query</code> is an MDX query. * * <p>It is created by calling {@link Connection#parseQuery}, * and executed by calling {@link Connection#execute}, * to return a {@link Result}.</p> * * <h3>Query control</h3> * * <p>Most queries are model citizens, executing quickly (often using cached * results from previous queries), but some queries take more time, or more * database resources, or more results, than is reasonable. Mondrian offers * three ways to control rogue queries:<ul> * * <li>You can set a query timeout by setting the * {@link MondrianProperties#QueryTimeout} parameter. If the query * takes longer to execute than the value of this parameter, the system * will kill it.</li> * * <li>The {@link MondrianProperties#QueryLimit} parameter limits the number * of cells returned by a query.</li> * * <li>At any time while a query is executing, another thread can cancel the * query by calling * {@link #getStatement()}.{@link Statement#cancel() cancel()}. * The call to {@link Connection#execute(Query)} * will throw an exception.</li> * * </ul> * * @author jhyde, 20 January, 1999 */ public class Query extends QueryPart { private Formula[] formulas; /** * public-private: This must be public because it is still accessed in * rolap.RolapConnection */ public QueryAxis[] axes; private QueryAxis slicerAxis; /** * Definitions of all parameters used in this query. */ private final List<Parameter> parameters = new ArrayList<Parameter>(); private final Map<String, Parameter> parametersByName = new HashMap<String, Parameter>(); /** * Cell properties. Not currently used. */ private final QueryPart[] cellProps; /** * Cube this query belongs to. */ private final Cube cube; private final Statement statement; public Calc[] axisCalcs; public Calc slicerCalc; /** * Set of FunDefs for which alerts about non-native evaluation * have already been posted. */ Set<FunDef> alertedNonNativeFunDefs; /** * Unique list of members referenced from the measures dimension. * Will be used to determine if cross joins can be processed natively * for virtual cubes. */ private Set<Member> measuresMembers; /** * If true, virtual cubes can be processed using native cross joins. * It defaults to true, unless functions are applied on measures. */ private boolean nativeCrossJoinVirtualCube; /** * Used for virtual cubes. * Comtains a list of base cubes related to a virtual cube */ private List<RolapCube> baseCubes; /** * If true, enforce validation even when ignoreInvalidMembers is set. */ private boolean strictValidation; /** * How should the query be returned? Valid values are: * ResultStyle.ITERABLE * ResultStyle.LIST * ResultStyle.MUTABLE_LIST * For java4, use LIST */ private ResultStyle resultStyle = Util.Retrowoven ? ResultStyle.LIST : ResultStyle.ITERABLE; private Map<String, Object> evalCache = new HashMap<String, Object>(); /** * List of aliased expressions defined in this query, and where they are * defined. There might be more than one aliased expression with the same * name. */ private final List<ScopedNamedSet> scopedNamedSets = new ArrayList<ScopedNamedSet>(); private boolean ownStatement; /** * Creates a Query. */ public Query(Statement statement, Formula[] formulas, QueryAxis[] axes, String cube, QueryAxis slicerAxis, QueryPart[] cellProps, boolean strictValidation) { this(statement, Util.lookupCube(statement.getSchemaReader(), cube, true), formulas, axes, slicerAxis, cellProps, new Parameter[0], strictValidation); } /** * Creates a Query. */ public Query(Statement statement, Cube mdxCube, Formula[] formulas, QueryAxis[] axes, QueryAxis slicerAxis, QueryPart[] cellProps, Parameter[] parameters, boolean strictValidation) { this.statement = statement; this.cube = mdxCube; this.formulas = formulas; this.axes = axes; normalizeAxes(); this.slicerAxis = slicerAxis; this.cellProps = cellProps; this.parameters.addAll(Arrays.asList(parameters)); this.measuresMembers = new HashSet<Member>(); // assume, for now, that cross joins on virtual cubes can be // processed natively; as we parse the query, we'll know otherwise this.nativeCrossJoinVirtualCube = true; this.strictValidation = strictValidation; this.alertedNonNativeFunDefs = new HashSet<FunDef>(); statement.setQuery(this); resolve(); if (RolapUtil.PROFILE_LOGGER.isDebugEnabled() && statement.getProfileHandler() == null) { statement.enableProfiling(new ProfileHandler() { public void explain(String plan, QueryTiming timing) { if (timing != null) { plan += "\n" + timing; } RolapUtil.PROFILE_LOGGER.debug(plan); } }); } } /** * Sets the timeout in milliseconds of this Query. * * <p>Zero means no timeout. * * @param queryTimeoutMillis Timeout in milliseconds * * @deprecated This method will be removed in mondrian-4.0 */ public void setQueryTimeoutMillis(long queryTimeoutMillis) { statement.setQueryTimeoutMillis(queryTimeoutMillis); } /** * Checks whether the property name is present in the query. */ public boolean hasCellProperty(String propertyName) { for (QueryPart cellProp : cellProps) { if (((CellProperty) cellProp).isNameEquals(propertyName)) { return true; } } return false; } /** * Checks whether any cell property present in the query */ public boolean isCellPropertyEmpty() { return cellProps.length == 0; } /** * Adds a new formula specifying a set * to an existing query. */ public void addFormula(Id id, Exp exp) { addFormula(new Formula(false, id, exp, new MemberProperty[0], null, null)); } /** * Adds a new formula specifying a member * to an existing query. * * @param id Name of member * @param exp Expression for member * @param memberProperties Properties of member */ public void addFormula(Id id, Exp exp, MemberProperty[] memberProperties) { addFormula(new Formula(true, id, exp, memberProperties, null, null)); } /** * Adds a new formula specifying a member or a set * to an existing query; resolve is called after * the formula has been added. * * @param formula Formula to add to query */ public void addFormula(Formula formula) { formulas = Util.append(formulas, formula); resolve(); } /** * Adds some number of new formulas specifying members * or sets to an existing query; resolve is only called * once, after all the new members have been added to * the query. * * @param additions Formulas to add to query */ public void addFormulas(Formula... additions) { formulas = Util.appendArrays(formulas, additions); resolve(); } /** * Creates a validator for this query. * * @return Validator */ public Validator createValidator() { return createValidator(statement.getSchema().getFunTable(), false); } /** * Creates a validator for this query that uses a given function table and * function validation policy. * * @param functionTable Function table * @param alwaysResolveFunDef Whether to always resolve function * definitions (see {@link Validator#alwaysResolveFunDef()}) * @return Validator */ public Validator createValidator(FunTable functionTable, boolean alwaysResolveFunDef) { return new QueryValidator(functionTable, alwaysResolveFunDef, Query.this); } /** * @deprecated Please use {@link #clone}; this method will be removed in * mondrian-4.0 */ public Query safeClone() { return clone(); } @SuppressWarnings({ "CloneDoesntCallSuperClone", "CloneDoesntDeclareCloneNotSupportedException" }) public Query clone() { return new Query(statement, cube, Formula.cloneArray(formulas), QueryAxis.cloneArray(axes), (slicerAxis == null) ? null : (QueryAxis) slicerAxis.clone(), cellProps, parameters.toArray(new Parameter[parameters.size()]), strictValidation); } public Connection getConnection() { return statement.getMondrianConnection(); } /** * Issues a cancel request on this Query object. Once the thread * running the query detects the cancel request, the query execution will * throw an exception. See <code>BasicQueryTest.testCancel</code> for an * example of usage of this method. * * @deprecated This method is deprecated and will be removed in mondrian-4.0 */ public void cancel() { try { statement.cancel(); } catch (SQLException e) { throw new RuntimeException(e); } } /** * Checks if either a cancel request has been issued on the query or * the execution time has exceeded the timeout value (if one has been * set). Exceptions are raised if either of these two conditions are * met. This method should be called periodically during query execution * to ensure timely detection of these events, particularly before/after * any potentially long running operations. * * @deprecated This method will be removed in mondrian-4.0 */ public void checkCancelOrTimeout() { final Execution execution0 = statement.getCurrentExecution(); if (execution0 == null) { return; } execution0.checkCancelOrTimeout(); } /** * Gets the query start time * @return start time * * @deprecated Use {@link Execution#getStartTime}. This method is deprecated * and will be removed in mondrian-4.0 */ public long getQueryStartTime() { final Execution currentExecution = statement.getCurrentExecution(); return currentExecution == null ? 0 : currentExecution.getStartTime(); } /** * Determines whether an alert for non-native evaluation needs * to be posted. * * @param funDef function type to alert for * * @return true if alert should be raised */ public boolean shouldAlertForNonNative(FunDef funDef) { return alertedNonNativeFunDefs.add(funDef); } private void normalizeAxes() { for (int i = 0; i < axes.length; i++) { AxisOrdinal correctOrdinal = AxisOrdinal.StandardAxisOrdinal.forLogicalOrdinal(i); if (axes[i].getAxisOrdinal() != correctOrdinal) { for (int j = i + 1; j < axes.length; j++) { if (axes[j].getAxisOrdinal() == correctOrdinal) { // swap axes QueryAxis temp = axes[i]; axes[i] = axes[j]; axes[j] = temp; break; } } } } } /** * Performs type-checking and validates internal consistency of a query, * using the default resolver. * * <p>This method is called automatically when a query is created; you need * to call this method manually if you have modified the query's expression * tree in any way. */ public void resolve() { final Validator validator = createValidator(); resolve(validator); // resolve self and children // Create a dummy result so we can use its evaluator final Evaluator evaluator = RolapUtil.createEvaluator(statement); ExpCompiler compiler = createCompiler(evaluator, validator, Collections.singletonList(resultStyle)); compile(compiler); } /** * @return true if the relevant property for ignoring invalid members is * set to true for this query's environment (a different property is * checked depending on whether environment is schema load vs query * validation) */ public boolean ignoreInvalidMembers() { MondrianProperties props = MondrianProperties.instance(); final boolean load = ((RolapCube) getCube()).isLoadInProgress(); return !strictValidation && (load ? props.IgnoreInvalidMembers.get() : props.IgnoreInvalidMembersDuringQuery.get()); } /** * A Query's ResultStyle can only be one of the following: * ResultStyle.ITERABLE * ResultStyle.LIST * ResultStyle.MUTABLE_LIST */ public void setResultStyle(ResultStyle resultStyle) { switch (resultStyle) { case ITERABLE: // For java4, use LIST this.resultStyle = (Util.Retrowoven) ? ResultStyle.LIST : ResultStyle.ITERABLE; break; case LIST: case MUTABLE_LIST: this.resultStyle = resultStyle; break; default: throw ResultStyleException.generateBadType(ResultStyle.ITERABLE_LIST_MUTABLELIST, resultStyle); } } public ResultStyle getResultStyle() { return resultStyle; } /** * Generates compiled forms of all expressions. * * @param compiler Compiler */ private void compile(ExpCompiler compiler) { if (formulas != null) { for (Formula formula : formulas) { formula.compile(); } } if (axes != null) { axisCalcs = new Calc[axes.length]; for (int i = 0; i < axes.length; i++) { axisCalcs[i] = axes[i].compile(compiler, resultStyle); } } if (slicerAxis != null) { slicerCalc = slicerAxis.compile(compiler, resultStyle); } } /** * Performs type-checking and validates internal consistency of a query. * * @param validator Validator */ public void resolve(Validator validator) { // Before commencing validation, create all calculated members, // calculated sets, and parameters. if (formulas != null) { // Resolving of formulas should be done in two parts // because formulas might depend on each other, so all calculated // mdx elements have to be defined during resolve. for (Formula formula : formulas) { formula.createElement(validator.getQuery()); } } // Register all parameters. parameters.clear(); parametersByName.clear(); accept(new ParameterFinder()); // Register all aliased expressions ('expr AS alias') as named sets. accept(new AliasedExpressionFinder()); // Validate formulas. if (formulas != null) { for (Formula formula : formulas) { validator.validate(formula); } } // Validate axes. if (axes != null) { Set<Integer> axisNames = new HashSet<Integer>(); for (QueryAxis axis : axes) { validator.validate(axis); if (!axisNames.add(axis.getAxisOrdinal().logicalOrdinal())) { throw MondrianResource.instance().DuplicateAxis.ex(axis.getAxisName()); } } // Make sure that there are no gaps. If there are N axes, then axes // 0 .. N-1 should exist. int seekOrdinal = AxisOrdinal.StandardAxisOrdinal.COLUMNS.logicalOrdinal(); for (QueryAxis axis : axes) { if (!axisNames.contains(seekOrdinal)) { AxisOrdinal axisName = AxisOrdinal.StandardAxisOrdinal.forLogicalOrdinal(seekOrdinal); throw MondrianResource.instance().NonContiguousAxis.ex(seekOrdinal, axisName.name()); } ++seekOrdinal; } } if (slicerAxis != null) { slicerAxis.validate(validator); } // Make sure that no hierarchy is used on more than one axis. for (Hierarchy hierarchy : ((RolapCube) getCube()).getHierarchies()) { int useCount = 0; for (QueryAxis axis : allAxes()) { if (axis.getSet().getType().usesHierarchy(hierarchy, true)) { ++useCount; } } if (useCount > 1) { throw MondrianResource.instance().HierarchyInIndependentAxes.ex(hierarchy.getUniqueName()); } } } @Override public void explain(PrintWriter pw) { final boolean profiling = getStatement().getProfileHandler() != null; final CalcWriter calcWriter = new CalcWriter(pw, profiling); for (Formula formula : formulas) { formula.getMdxMember(); // TODO: } if (slicerCalc != null) { pw.println("Axis (FILTER):"); slicerCalc.accept(calcWriter); pw.println(); } int i = -1; for (QueryAxis axis : axes) { ++i; pw.println("Axis (" + axis.getAxisName() + "):"); axisCalcs[i].accept(calcWriter); pw.println(); } pw.flush(); } /** * Returns a collection of all axes, including the slicer as the first * element, if there is a slicer. * * @return Collection of all axes including slicer */ private Collection<QueryAxis> allAxes() { if (slicerAxis == null) { return Arrays.asList(axes); } else { //noinspection unchecked return new CompositeCollection( new Collection[] { Collections.singletonList(slicerAxis), Arrays.asList(axes) }); } } public void unparse(PrintWriter pw) { if (formulas != null) { for (int i = 0; i < formulas.length; i++) { if (i == 0) { pw.print("with "); } else { pw.print(" "); } formulas[i].unparse(pw); pw.println(); } } pw.print("select "); if (axes != null) { for (int i = 0; i < axes.length; i++) { axes[i].unparse(pw); if (i < axes.length - 1) { pw.println(","); pw.print(" "); } else { pw.println(); } } } if (cube != null) { pw.println("from [" + cube.getName() + "]"); } if (slicerAxis != null) { pw.print("where "); slicerAxis.unparse(pw); pw.println(); } } /** Returns the MDX query string. */ public String toString() { resolve(); return Util.unparse(this); } public Object[] getChildren() { // Chidren are axes, slicer, and formulas (in that order, to be // consistent with replaceChild). List<QueryPart> list = new ArrayList<QueryPart>(); list.addAll(Arrays.asList(axes)); if (slicerAxis != null) { list.add(slicerAxis); } list.addAll(Arrays.asList(formulas)); return list.toArray(); } public QueryAxis getSlicerAxis() { return slicerAxis; } public void setSlicerAxis(QueryAxis axis) { this.slicerAxis = axis; } /** * Adds a level to an axis expression. */ public void addLevelToAxis(AxisOrdinal axis, Level level) { assert axis != null; axes[axis.logicalOrdinal()].addLevel(level); } /** * Returns the hierarchies in an expression. * * <p>If the expression's type is a dimension with several hierarchies, * assumes that the expression yields a member of the first (default) * hierarchy of the dimension. * * <p>For example, the expression * <blockquote><code>Crossjoin( * Hierarchize( * Union( * {[Time].LastSibling}, [Time].LastSibling.Children)), * {[Measures].[Unit Sales], [Measures].[Store Cost]})</code> * </blockquote> * * has type <code>{[Time.Monthly], [Measures]}</code> even though * <code>[Time].LastSibling</code> might return a member of either * [Time.Monthly] or [Time.Weekly]. */ private Hierarchy[] collectHierarchies(Exp queryPart) { Type exprType = queryPart.getType(); if (exprType instanceof SetType) { exprType = ((SetType) exprType).getElementType(); } if (exprType instanceof TupleType) { final Type[] types = ((TupleType) exprType).elementTypes; ArrayList<Hierarchy> hierarchyList = new ArrayList<Hierarchy>(); for (Type type : types) { hierarchyList.add(getTypeHierarchy(type)); } return hierarchyList.toArray(new Hierarchy[hierarchyList.size()]); } return new Hierarchy[] { getTypeHierarchy(exprType) }; } private Hierarchy getTypeHierarchy(final Type type) { Hierarchy hierarchy = type.getHierarchy(); if (hierarchy != null) { return hierarchy; } final Dimension dimension = type.getDimension(); if (dimension != null) { return dimension.getHierarchy(); } return null; } /** * Assigns a value to the parameter with a given name. * * @throws RuntimeException if there is not parameter with the given name */ public void setParameter(final String parameterName, final Object value) { // Need to resolve query before we set parameters, in order to create // slots to store them in. (This code will go away when parameters // belong to prepared statements.) if (parameters.isEmpty()) { resolve(); } final Parameter param = getSchemaReader(false).getParameter(parameterName); if (param == null) { throw MondrianResource.instance().UnknownParameter.ex(parameterName); } if (!param.isModifiable()) { throw MondrianResource.instance().ParameterIsNotModifiable.ex(parameterName, param.getScope().name()); } final Object value2 = Locus.execute(new Execution(statement, 0), "Query.quickParse", new Locus.Action<Object>() { public Object execute() { return quickParse(parameterName, param.getType(), value, Query.this); } }); param.setValue(value2); } /** * Converts a value into something appropriate for a given type. * * <p>Viz: * <ul> * <li>For numerics, takes number or string and returns a {@link Number}. * <li>For strings, takes string, or calls {@link Object#toString()} on any * other type * <li>For members, takes member or string * <li>For sets of members, requires a list of members or strings and * converts each element to a member. * </ul> * * @param type Type * @param value Value * @param query Query * @return Value of appropriate type * @throws NumberFormatException If value needs to be a number but isn't */ private static Object quickParse(String parameterName, Type type, Object value, Query query) throws NumberFormatException { int category = TypeUtil.typeToCategory(type); switch (category) { case Category.Numeric: if (value instanceof Number || value == null) { return value; } if (value instanceof String) { String s = (String) value; try { return new Integer(s); } catch (NumberFormatException e) { return new Double(s); } } throw Util.newInternal( "Invalid value '" + value + "' for parameter '" + parameterName + "', type " + type); case Category.String: if (value == null) { return null; } return value.toString(); case Category.Set: if (value instanceof String) { value = IdentifierParser.parseIdentifierList((String) value); } if (!(value instanceof List)) { throw Util.newInternal( "Invalid value '" + value + "' for parameter '" + parameterName + "', type " + type); } List<Member> expList = new ArrayList<Member>(); final List list = (List) value; final SetType setType = (SetType) type; final Type elementType = setType.getElementType(); for (Object o : list) { // In keeping with MDX semantics, null members are omitted from // lists. if (o == null) { continue; } final Member member = (Member) quickParse(parameterName, elementType, o, query); expList.add(member); } return expList; case Category.Member: if (value == null) { // Setting a member parameter to null is the same as setting to // the null member of the hierarchy. May not be equivalent to // the default value of the parameter, nor the same as the all // member. if (type.getHierarchy() != null) { value = type.getHierarchy().getNullMember(); } else if (type.getDimension() != null) { value = type.getDimension().getHierarchy().getNullMember(); } } if (value instanceof String) { value = Util.parseIdentifier((String) value); } if (value instanceof List && Util.canCast((List) value, Id.Segment.class)) { final List<Id.Segment> segmentList = Util.cast((List) value); final OlapElement olapElement = Util.lookup(query, segmentList); if (olapElement instanceof Member) { value = olapElement; } } if (value instanceof List && Util.canCast((List) value, IdentifierSegment.class)) { final List<IdentifierSegment> olap4jSegmentList = Util.cast((List) value); final List<Id.Segment> segmentList = Util.convert(olap4jSegmentList); final OlapElement olapElement = Util.lookup(query, segmentList); if (olapElement instanceof Member) { value = olapElement; } } if (value instanceof Member) { if (type.isInstance(value)) { return value; } } throw Util.newInternal( "Invalid value '" + value + "' for parameter '" + parameterName + "', type " + type); default: throw Category.instance.badValue(category); } } /** * Swaps the x- and y- axes. * Does nothing if the number of axes != 2. */ public void swapAxes() { if (axes.length == 2) { Exp e0 = axes[0].getSet(); boolean nonEmpty0 = axes[0].isNonEmpty(); Exp e1 = axes[1].getSet(); boolean nonEmpty1 = axes[1].isNonEmpty(); axes[1].setSet(e0); axes[1].setNonEmpty(nonEmpty0); axes[0].setSet(e1); axes[0].setNonEmpty(nonEmpty1); // showSubtotals ??? } } /** * Returns the parameters defined in this query. */ public Parameter[] getParameters() { return parameters.toArray(new Parameter[parameters.size()]); } public Cube getCube() { return cube; } /** * Returns a schema reader. * * @param accessControlled If true, schema reader returns only elements * which are accessible to the statement's current role * * @return schema reader */ public SchemaReader getSchemaReader(boolean accessControlled) { final Role role; if (accessControlled) { // full access control role = getConnection().getRole(); } else { role = null; } final SchemaReader cubeSchemaReader = cube.getSchemaReader(role); return new QuerySchemaReader(cubeSchemaReader, Query.this); } /** * Looks up a member whose unique name is <code>memberUniqueName</code> * from cache. If the member is not in cache, returns null. */ public Member lookupMemberFromCache(String memberUniqueName) { // first look in defined members for (Member member : getDefinedMembers()) { if (Util.equalName(member.getUniqueName(), memberUniqueName) || Util.equalName(getUniqueNameWithoutAll(member), memberUniqueName)) { return member; } } return null; } private String getUniqueNameWithoutAll(Member member) { // build unique string Member parentMember = member.getParentMember(); if ((parentMember != null) && !parentMember.isAll()) { return Util.makeFqName(getUniqueNameWithoutAll(parentMember), member.getName()); } else { return Util.makeFqName(member.getHierarchy(), member.getName()); } } /** * Looks up a named set. */ private NamedSet lookupNamedSet(Id.Segment segment) { if (!(segment instanceof Id.NameSegment)) { return null; } Id.NameSegment nameSegment = (Id.NameSegment) segment; for (Formula formula : formulas) { if (!formula.isMember() && formula.getElement() != null && formula.getName().equals(nameSegment.getName())) { return (NamedSet) formula.getElement(); } } return null; } /** * Creates a named set defined by an alias. */ public ScopedNamedSet createScopedNamedSet(String name, QueryPart scope, Exp expr) { final ScopedNamedSet scopedNamedSet = new ScopedNamedSet(name, scope, expr); scopedNamedSets.add(scopedNamedSet); return scopedNamedSet; } /** * Looks up a named set defined by an alias. * * @param nameParts Multi-part identifier for set * @param scopeList Parse tree node where name is used (last in list) and */ ScopedNamedSet lookupScopedNamedSet(List<Id.Segment> nameParts, ArrayStack<QueryPart> scopeList) { if (nameParts.size() != 1) { return null; } if (!(nameParts.get(0) instanceof Id.NameSegment)) { return null; } String name = ((Id.NameSegment) nameParts.get(0)).getName(); ScopedNamedSet bestScopedNamedSet = null; int bestScopeOrdinal = -1; for (ScopedNamedSet scopedNamedSet : scopedNamedSets) { if (Util.equalName(scopedNamedSet.name, name)) { int scopeOrdinal = scopeList.indexOf(scopedNamedSet.scope); if (scopeOrdinal > bestScopeOrdinal) { bestScopedNamedSet = scopedNamedSet; bestScopeOrdinal = scopeOrdinal; } } } return bestScopedNamedSet; } /** * Returns an array of the formulas used in this query. */ public Formula[] getFormulas() { return formulas; } /** * Returns an array of this query's axes. */ public QueryAxis[] getAxes() { return axes; } /** * Remove a formula from the query. If <code>failIfUsedInQuery</code> is * true, checks and throws an error if formula is used somewhere in the * query. */ public void removeFormula(String uniqueName, boolean failIfUsedInQuery) { Formula formula = findFormula(uniqueName); if (failIfUsedInQuery && formula != null) { OlapElement mdxElement = formula.getElement(); // search the query tree to see if this formula expression is used // anywhere (on the axes or in another formula) Walker walker = new Walker(this); while (walker.hasMoreElements()) { Object queryElement = walker.nextElement(); if (!queryElement.equals(mdxElement)) { continue; } // mdxElement is used in the query. lets find on on which axis // or formula String formulaType = formula.isMember() ? MondrianResource.instance().CalculatedMember.str() : MondrianResource.instance().CalculatedSet.str(); int i = 0; Object parent = walker.getAncestor(i); Object grandParent = walker.getAncestor(i + 1); while ((parent != null) && (grandParent != null)) { if (grandParent instanceof Query) { if (parent instanceof Axis) { throw MondrianResource.instance().MdxCalculatedFormulaUsedOnAxis.ex(formulaType, uniqueName, ((QueryAxis) parent).getAxisName()); } else if (parent instanceof Formula) { String parentFormulaType = ((Formula) parent).isMember() ? MondrianResource.instance().CalculatedMember.str() : MondrianResource.instance().CalculatedSet.str(); throw MondrianResource.instance().MdxCalculatedFormulaUsedInFormula.ex(formulaType, uniqueName, parentFormulaType, ((Formula) parent).getUniqueName()); } else { throw MondrianResource.instance().MdxCalculatedFormulaUsedOnSlicer.ex(formulaType, uniqueName); } } ++i; parent = walker.getAncestor(i); grandParent = walker.getAncestor(i + 1); } throw MondrianResource.instance().MdxCalculatedFormulaUsedInQuery.ex(formulaType, uniqueName, Util.unparse(this)); } } // remove formula from query List<Formula> formulaList = new ArrayList<Formula>(); for (Formula formula1 : formulas) { if (!formula1.getUniqueName().equalsIgnoreCase(uniqueName)) { formulaList.add(formula1); } } // it has been found and removed this.formulas = formulaList.toArray(new Formula[formulaList.size()]); } /** * Returns whether a formula can safely be removed from the query. It can be * removed if the member or set it defines it not used anywhere else in the * query, including in another formula. * * @param uniqueName Unique name of the member or set defined by the formula * @return whether the formula can safely be removed */ public boolean canRemoveFormula(String uniqueName) { Formula formula = findFormula(uniqueName); if (formula == null) { return false; } OlapElement mdxElement = formula.getElement(); // Search the query tree to see if this formula expression is used // anywhere (on the axes or in another formula). Walker walker = new Walker(this); while (walker.hasMoreElements()) { Object queryElement = walker.nextElement(); if (queryElement instanceof MemberExpr && ((MemberExpr) queryElement).getMember().equals(mdxElement)) { return false; } if (queryElement instanceof NamedSetExpr && ((NamedSetExpr) queryElement).getNamedSet().equals(mdxElement)) { return false; } } return true; } /** * Looks up a calculated member or set defined in this Query. * * @param uniqueName Unique name of calculated member or set * @return formula defining calculated member, or null if not found */ public Formula findFormula(String uniqueName) { for (Formula formula : formulas) { if (formula.getUniqueName().equalsIgnoreCase(uniqueName)) { return formula; } } return null; } /** * Finds formula by name and renames it to new name. */ public void renameFormula(String uniqueName, String newName) { Formula formula = findFormula(uniqueName); if (formula == null) { throw MondrianResource.instance().MdxFormulaNotFound.ex("formula", uniqueName, Util.unparse(this)); } formula.rename(newName); } List<Member> getDefinedMembers() { List<Member> definedMembers = new ArrayList<Member>(); for (final Formula formula : formulas) { if (formula.isMember() && formula.getElement() != null && getConnection().getRole().canAccess(formula.getElement())) { definedMembers.add((Member) formula.getElement()); } } return definedMembers; } /** * Finds axis by index and sets flag to show empty cells on that axis. */ public void setAxisShowEmptyCells(int axis, boolean showEmpty) { if (axis >= axes.length) { throw MondrianResource.instance().MdxAxisShowSubtotalsNotSupported.ex(axis); } axes[axis].setNonEmpty(!showEmpty); } /** * Returns <code>Hierarchy[]</code> used on <code>axis</code>. It calls * {@link #collectHierarchies}. */ public Hierarchy[] getMdxHierarchiesOnAxis(AxisOrdinal axis) { if (axis.logicalOrdinal() >= axes.length) { throw MondrianResource.instance().MdxAxisShowSubtotalsNotSupported.ex(axis.logicalOrdinal()); } QueryAxis queryAxis = axis.isFilter() ? slicerAxis : axes[axis.logicalOrdinal()]; return collectHierarchies(queryAxis.getSet()); } /** * Compiles an expression, using a cached compiled expression if available. * * @param exp Expression * @param scalar Whether expression is scalar * @param resultStyle Preferred result style; if null, use query's default * result style; ignored if expression is scalar * @return compiled expression */ public Calc compileExpression(Exp exp, boolean scalar, ResultStyle resultStyle) { // REVIEW: Set query on a connection's shared internal statement is // not re-entrant. statement.setQuery(this); Evaluator evaluator = RolapEvaluator.create(statement); final Validator validator = createValidator(); List<ResultStyle> resultStyleList; resultStyleList = Collections.singletonList(resultStyle != null ? resultStyle : this.resultStyle); final ExpCompiler compiler = createCompiler(evaluator, validator, resultStyleList); if (scalar) { return compiler.compileScalar(exp, false); } else { return compiler.compile(exp); } } public ExpCompiler createCompiler() { // REVIEW: Set query on a connection's shared internal statement is // not re-entrant. statement.setQuery(this); Evaluator evaluator = RolapEvaluator.create(statement); Validator validator = createValidator(); return createCompiler(evaluator, validator, Collections.singletonList(resultStyle)); } private ExpCompiler createCompiler(final Evaluator evaluator, final Validator validator, List<ResultStyle> resultStyleList) { ExpCompiler compiler = ExpCompiler.Factory.getExpCompiler(evaluator, validator, resultStyleList); final int expDeps = MondrianProperties.instance().TestExpDependencies.get(); final ProfileHandler profileHandler = statement.getProfileHandler(); if (profileHandler != null) { // Cannot test dependencies and profile at the same time. Profiling // trumps. compiler = RolapUtil.createProfilingCompiler(compiler); } else if (expDeps > 0) { compiler = RolapUtil.createDependencyTestingCompiler(compiler); } return compiler; } /** * Keeps track of references to members of the measures dimension * * @param olapElement potential measure member */ public void addMeasuresMembers(OlapElement olapElement) { if (olapElement instanceof Member) { Member member = (Member) olapElement; if (member.isMeasure()) { measuresMembers.add(member); } } } /** * @return set of members from the measures dimension referenced within * this query */ public Set<Member> getMeasuresMembers() { return Collections.unmodifiableSet(measuresMembers); } /** * Indicates that the query cannot use native cross joins to process * this virtual cube */ public void setVirtualCubeNonNativeCrossJoin() { nativeCrossJoinVirtualCube = false; } /** * @return true if the query can use native cross joins on a virtual * cube */ public boolean nativeCrossJoinVirtualCube() { return nativeCrossJoinVirtualCube; } /** * Saves away the base cubes related to the virtual cube * referenced in this query * * @param baseCubes set of base cubes */ public void setBaseCubes(List<RolapCube> baseCubes) { this.baseCubes = baseCubes; } /** * return the set of base cubes associated with the virtual cube referenced * in this query * * @return set of base cubes */ public List<RolapCube> getBaseCubes() { return baseCubes; } public Object accept(MdxVisitor visitor) { Object o = visitor.visit(this); if (visitor.shouldVisitChildren()) { // visit formulas for (Formula formula : formulas) { formula.accept(visitor); } // visit axes for (QueryAxis axis : axes) { axis.accept(visitor); } if (slicerAxis != null) { slicerAxis.accept(visitor); } } return o; } /** * Put an Object value into the evaluation cache with given key. * This is used by Calc's to store information between iterations * (rather than re-generate each time). * * @param key the cache key * @param value the cache value */ public void putEvalCache(String key, Object value) { evalCache.put(key, value); } /** * Gets the Object associated with the value. * * @param key the cache key * @return the cached value or null. */ public Object getEvalCache(String key) { return evalCache.get(key); } /** * Remove all entries in the evaluation cache */ public void clearEvalCache() { evalCache.clear(); } /** * Closes this query. * * <p>Releases any resources held. Writes statistics to log if profiling * is enabled. * * <p>This method is idempotent. * * @deprecated This method will be removed in mondrian-4.0. */ public void close() { if (ownStatement) { statement.close(); } } public Statement getStatement() { return statement; } /** * Sets that the query owns its statement; therefore it will need to * close it when the query is closed. * * @param ownStatement Whether the statement belongs to the query */ public void setOwnStatement(boolean ownStatement) { this.ownStatement = ownStatement; } /** * Source of metadata within the scope of a query. * * <p>Note especially that {@link #getCalculatedMember(java.util.List)} * returns the calculated members defined in this query. It does not * perform access control; all calculated members defined in a query are * visible to everyone. */ private static class QuerySchemaReader extends DelegatingSchemaReader implements NameResolver.Namespace { private final Query query; public QuerySchemaReader(SchemaReader cubeSchemaReader, Query query) { super(cubeSchemaReader); this.query = query; } public SchemaReader withoutAccessControl() { return new QuerySchemaReader(schemaReader.withoutAccessControl(), query); } public Member getMemberByUniqueName(List<Id.Segment> uniqueNameParts, boolean failIfNotFound, MatchType matchType) { final String uniqueName = Util.implode(uniqueNameParts); Member member = query.lookupMemberFromCache(uniqueName); if (member == null) { // Not a calculated member in the query, so go to the cube. member = schemaReader.getMemberByUniqueName(uniqueNameParts, failIfNotFound, matchType); } if (!failIfNotFound && member == null) { return null; } if (getRole().canAccess(member)) { return member; } else { return null; } } public List<Member> getLevelMembers(Level level, boolean includeCalculated) { List<Member> members = super.getLevelMembers(level, false); if (includeCalculated) { members = Util.addLevelCalculatedMembers(this, level, members); } return members; } public Member getCalculatedMember(List<Id.Segment> nameParts) { for (final Formula formula : query.formulas) { if (!formula.isMember()) { continue; } Member member = (Member) formula.getElement(); if (member == null) { continue; } if (!match(member, nameParts)) { continue; } if (!query.getConnection().getRole().canAccess(member)) { continue; } return member; } return null; } private static boolean match(Member member, List<Id.Segment> nameParts) { if (Util.equalName(Util.implode(nameParts), member.getUniqueName())) { // exact match return true; } Id.Segment segment = nameParts.get(nameParts.size() - 1); while (member.getParentMember() != null) { if (!segment.matches(member.getName())) { return false; } member = member.getParentMember(); nameParts = nameParts.subList(0, nameParts.size() - 1); segment = nameParts.get(nameParts.size() - 1); } if (segment.matches(member.getName())) { return Util.equalName(member.getHierarchy().getUniqueName(), Util.implode(nameParts.subList(0, nameParts.size() - 1))); } else if (member.isAll()) { return Util.equalName(member.getHierarchy().getUniqueName(), Util.implode(nameParts)); } else { return false; } } public List<Member> getCalculatedMembers(Hierarchy hierarchy) { List<Member> result = new ArrayList<Member>(); // Add calculated members in the cube. final List<Member> calculatedMembers = super.getCalculatedMembers(hierarchy); result.addAll(calculatedMembers); // Add calculated members defined in the query. for (Member member : query.getDefinedMembers()) { if (member.getHierarchy().equals(hierarchy)) { result.add(member); } } return result; } public List<Member> getCalculatedMembers(Level level) { List<Member> hierarchyMembers = getCalculatedMembers(level.getHierarchy()); List<Member> result = new ArrayList<Member>(); for (Member member : hierarchyMembers) { if (member.getLevel().equals(level)) { result.add(member); } } return result; } public List<Member> getCalculatedMembers() { return query.getDefinedMembers(); } public OlapElement getElementChild(OlapElement parent, Id.Segment s) { return getElementChild(parent, s, MatchType.EXACT); } public OlapElement getElementChild(OlapElement parent, Id.Segment s, MatchType matchType) { // first look in cube OlapElement mdxElement = schemaReader.getElementChild(parent, s, matchType); if (mdxElement != null) { return mdxElement; } // then look in defined members (fixes MONDRIAN-77) // then in defined sets if (!(s instanceof Id.NameSegment)) { return null; } String name = ((Id.NameSegment) s).getName(); for (Formula formula : query.formulas) { if (formula.isMember()) { continue; // have already done these } Id id = formula.getIdentifier(); if (id.getSegments().size() == 1 && id.getSegments().get(0).matches(name)) { return formula.getNamedSet(); } } return mdxElement; } @Override public OlapElement lookupCompoundInternal(OlapElement parent, List<Id.Segment> names, boolean failIfNotFound, int category, MatchType matchType) { if (matchType == MatchType.EXACT) { OlapElement oe = lookupCompound(parent, names, failIfNotFound, category, MatchType.EXACT_SCHEMA); if (oe != null) { return oe; } } // First look to ourselves. switch (category) { case Category.Unknown: case Category.Member: if (parent == query.cube) { final Member calculatedMember = getCalculatedMember(names); if (calculatedMember != null) { return calculatedMember; } } } switch (category) { case Category.Unknown: case Category.Set: if (parent == query.cube) { final NamedSet namedSet = getNamedSet(names); if (namedSet != null) { return namedSet; } } } // Then delegate to the next reader. OlapElement olapElement = super.lookupCompoundInternal(parent, names, failIfNotFound, category, matchType); if (olapElement instanceof Member) { Member member = (Member) olapElement; final Formula formula = (Formula) member.getPropertyValue(Property.FORMULA.name); if (formula != null) { // This is a calculated member defined against the cube. // Create a free-standing formula using the same // expression, then use the member defined in that formula. final Formula formulaClone = (Formula) formula.clone(); formulaClone.createElement(query); formulaClone.accept(query.createValidator()); olapElement = formulaClone.getMdxMember(); } } return olapElement; } public NamedSet getNamedSet(List<Id.Segment> nameParts) { if (nameParts.size() != 1) { return null; } return query.lookupNamedSet(nameParts.get(0)); } public Parameter getParameter(String name) { // Look for a parameter defined in the query. for (Parameter parameter : query.parameters) { if (parameter.getName().equals(name)) { return parameter; } } // Look for a parameter defined in this statement. if (Util.lookup(RolapConnectionProperties.class, name) != null) { Object value = query.statement.getProperty(name); // TODO: Don't assume it's a string. // TODO: Create expression which will get the value from the // statement at the time the query is executed. Literal defaultValue = Literal.createString(String.valueOf(value)); return new ConnectionParameterImpl(name, defaultValue); } return super.getParameter(name); } public OlapElement lookupChild(OlapElement parent, IdentifierSegment segment, MatchType matchType) { // ignore matchType return lookupChild(parent, segment); } public OlapElement lookupChild(OlapElement parent, IdentifierSegment segment) { // Only look for calculated members and named sets defined in the // query. for (Formula formula : query.getFormulas()) { if (NameResolver.matches(formula, parent, segment)) { return formula.getElement(); } } return null; } public List<NameResolver.Namespace> getNamespaces() { final List<NameResolver.Namespace> list = new ArrayList<NameResolver.Namespace>(); list.add(this); list.addAll(super.getNamespaces()); return list; } } private static class ConnectionParameterImpl extends ParameterImpl { public ConnectionParameterImpl(String name, Literal defaultValue) { super(name, defaultValue, "Connection property", new StringType()); } public Scope getScope() { return Scope.Connection; } public void setValue(Object value) { throw MondrianResource.instance().ParameterIsNotModifiable.ex(getName(), getScope().name()); } } /** * Implementation of {@link mondrian.olap.Validator} that works within a * particular query. * * <p>It's unlikely that we would want a validator that is * NOT within a particular query, but by organizing the code this way, with * the majority of the code in {@link mondrian.olap.ValidatorImpl}, the * dependencies between Validator and Query are explicit. */ private static class QueryValidator extends ValidatorImpl { private final boolean alwaysResolveFunDef; private Query query; private final SchemaReader schemaReader; /** * Creates a QueryValidator. * * @param functionTable Function table * @param alwaysResolveFunDef Whether to always resolve function * definitions (see {@link #alwaysResolveFunDef()}) * @param query Query */ public QueryValidator(FunTable functionTable, boolean alwaysResolveFunDef, Query query) { super(functionTable); this.alwaysResolveFunDef = alwaysResolveFunDef; this.query = query; this.schemaReader = new ScopedSchemaReader(this, true); } public SchemaReader getSchemaReader() { return schemaReader; } protected void defineParameter(Parameter param) { final String name = param.getName(); query.parameters.add(param); query.parametersByName.put(name, param); } public Query getQuery() { return query; } public boolean alwaysResolveFunDef() { return alwaysResolveFunDef; } public ArrayStack<QueryPart> getScopeStack() { return stack; } } /** * Schema reader that depends on the current scope during the validation * of a query. Depending on the scope, different calculated sets may be * visible. The scope is represented by the expression stack inside the * validator. */ private static class ScopedSchemaReader extends DelegatingSchemaReader implements NameResolver.Namespace { private final QueryValidator queryValidator; private final boolean accessControlled; /** * Creates a ScopedSchemaReader. * * @param queryValidator Validator that is being used to validate the * query * @param accessControlled Access controlled */ private ScopedSchemaReader(QueryValidator queryValidator, boolean accessControlled) { super(queryValidator.getQuery().getSchemaReader(accessControlled)); this.queryValidator = queryValidator; this.accessControlled = accessControlled; } public SchemaReader withoutAccessControl() { if (!accessControlled) { return this; } return new ScopedSchemaReader(queryValidator, false); } public List<NameResolver.Namespace> getNamespaces() { final List<NameResolver.Namespace> list = new ArrayList<NameResolver.Namespace>(); list.add(this); list.addAll(super.getNamespaces()); return list; } @Override public OlapElement lookupCompoundInternal(OlapElement parent, final List<Id.Segment> names, boolean failIfNotFound, int category, MatchType matchType) { switch (category) { case Category.Set: case Category.Unknown: final ScopedNamedSet namedSet = queryValidator.getQuery().lookupScopedNamedSet(names, queryValidator.getScopeStack()); if (namedSet != null) { return namedSet; } } return super.lookupCompoundInternal(parent, names, failIfNotFound, category, matchType); } public OlapElement lookupChild(OlapElement parent, IdentifierSegment segment, MatchType matchType) { // ignore matchType return lookupChild(parent, segment); } public OlapElement lookupChild(OlapElement parent, IdentifierSegment segment) { if (!(parent instanceof Cube)) { return null; } return queryValidator.getQuery().lookupScopedNamedSet(Collections.singletonList(Util.convert(segment)), queryValidator.getScopeStack()); } } public static class ScopedNamedSet implements NamedSet { private final String name; private final QueryPart scope; private Exp expr; /** * Creates a ScopedNamedSet. * * @param name Name * @param scope Scope of named set (the function call that encloses * the 'expr AS name', often GENERATE or FILTER) * @param expr Expression that defines the set */ private ScopedNamedSet(String name, QueryPart scope, Exp expr) { this.name = name; this.scope = scope; this.expr = expr; } public String getName() { return name; } public String getNameUniqueWithinQuery() { return System.identityHashCode(this) + ""; } public boolean isDynamic() { return true; } public Exp getExp() { return expr; } public void setExp(Exp expr) { this.expr = expr; } public void setName(String newName) { throw new UnsupportedOperationException(); } public Type getType() { return expr.getType(); } public Map<String, Annotation> getAnnotationMap() { return Collections.emptyMap(); } public NamedSet validate(Validator validator) { Exp newExpr = expr.accept(validator); final Type type = newExpr.getType(); if (type instanceof MemberType || type instanceof TupleType) { newExpr = new UnresolvedFunCall("{}", Syntax.Braces, new Exp[] { newExpr }).accept(validator); } this.expr = newExpr; return this; } public String getUniqueName() { return name; } public String getDescription() { throw new UnsupportedOperationException(); } public OlapElement lookupChild(SchemaReader schemaReader, Id.Segment s, MatchType matchType) { throw new UnsupportedOperationException(); } public String getQualifiedName() { throw new UnsupportedOperationException(); } public String getCaption() { throw new UnsupportedOperationException(); } public boolean isVisible() { throw new UnsupportedOperationException(); } public Hierarchy getHierarchy() { throw new UnsupportedOperationException(); } public Dimension getDimension() { throw new UnsupportedOperationException(); } public String getLocalized(LocalizedProperty prop, Locale locale) { throw new UnsupportedOperationException(); } } /** * Visitor that locates and registers parameters. */ private class ParameterFinder extends MdxVisitorImpl { public Object visit(ParameterExpr parameterExpr) { Parameter parameter = parameterExpr.getParameter(); if (!parameters.contains(parameter)) { parameters.add(parameter); parametersByName.put(parameter.getName(), parameter); } return null; } public Object visit(UnresolvedFunCall call) { if (call.getFunName().equals("Parameter")) { // Is there already a parameter with this name? String parameterName = ParameterFunDef.getParameterName(call.getArgs()); if (parametersByName.get(parameterName) != null) { throw MondrianResource.instance().ParameterDefinedMoreThanOnce.ex(parameterName); } Type type = ParameterFunDef.getParameterType(call.getArgs()); // Create a temporary parameter. We don't know its // type yet. The default of NULL is temporary. Parameter parameter = new ParameterImpl(parameterName, Literal.nullValue, null, type); parameters.add(parameter); parametersByName.put(parameterName, parameter); } return null; } } /** * Visitor that locates and registers all aliased expressions * ('expr AS alias') as named sets. The resulting named sets have scope, * therefore they can only be seen and used within that scope. */ private class AliasedExpressionFinder extends MdxVisitorImpl { @Override public Object visit(QueryAxis queryAxis) { registerAlias(queryAxis, queryAxis.getSet()); return super.visit(queryAxis); } public Object visit(UnresolvedFunCall call) { registerAliasArgs(call); return super.visit(call); } public Object visit(ResolvedFunCall call) { registerAliasArgs(call); return super.visit(call); } /** * Registers all arguments of a function that are named sets. * * @param call Function call */ private void registerAliasArgs(FunCall call) { for (Exp exp : call.getArgs()) { registerAlias((QueryPart) call, exp); } } /** * Registers a named set if an expression is of the form "expr AS * alias". * * @param parent Parent node * @param exp Expression that may be an "AS" */ private void registerAlias(QueryPart parent, Exp exp) { if (exp instanceof FunCall) { FunCall call2 = (FunCall) exp; if (call2.getSyntax() == Syntax.Infix && call2.getFunName().equals("AS")) { // Scope is the function enclosing the 'AS' expression. // For example, in // Filter(Time.Children AS s, x > y) // the scope of the set 's' is the Filter function. assert call2.getArgCount() == 2; if (call2.getArg(1) instanceof Id) { final Id id = (Id) call2.getArg(1); createScopedNamedSet(((Id.NameSegment) id.getSegments().get(0)).getName(), parent, call2.getArg(0)); } else if (call2.getArg(1) instanceof NamedSetExpr) { NamedSetExpr set = (NamedSetExpr) call2.getArg(1); createScopedNamedSet(set.getNamedSet().getName(), parent, call2.getArg(0)); } } } } } } // End Query.java