/* * JBoss, Home of Professional Open Source. * See the COPYRIGHT.txt file distributed with this work for information * regarding copyright ownership. Some portions may be licensed * to Red Hat, Inc. under one or more contributor license agreements. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * 02110-1301 USA. */ package org.teiid.query.optimizer.xml; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import org.teiid.api.exception.query.QueryMetadataException; import org.teiid.api.exception.query.QueryPlannerException; import org.teiid.core.TeiidComponentException; import org.teiid.core.TeiidRuntimeException; import org.teiid.query.mapping.relational.QueryNode; import org.teiid.query.mapping.xml.MappingDocument; import org.teiid.query.mapping.xml.MappingNode; import org.teiid.query.mapping.xml.MappingSourceNode; import org.teiid.query.mapping.xml.MappingVisitor; import org.teiid.query.mapping.xml.Navigator; import org.teiid.query.mapping.xml.ResultSetInfo; import org.teiid.query.metadata.TempMetadataAdapter; import org.teiid.query.metadata.TempMetadataStore; import org.teiid.query.resolver.QueryResolver; import org.teiid.query.resolver.util.ResolverUtil; import org.teiid.query.sql.lang.Command; import org.teiid.query.sql.lang.Criteria; import org.teiid.query.sql.lang.Query; import org.teiid.query.sql.lang.SubqueryFromClause; import org.teiid.query.sql.lang.UnaryFromClause; import org.teiid.query.sql.symbol.AliasSymbol; import org.teiid.query.sql.symbol.ElementSymbol; import org.teiid.query.sql.symbol.Expression; import org.teiid.query.sql.symbol.GroupSymbol; import org.teiid.query.sql.symbol.Symbol; import org.teiid.query.sql.visitor.ElementCollectorVisitor; import org.teiid.query.sql.visitor.ExpressionMappingVisitor; import org.teiid.query.sql.visitor.SQLStringVisitor; /** * This visitor will take source node's QueryNode, move the the inputset criteria * specified on the QueryNode on to the Source Node's query. */ public class SourceNodePlannerVisitor extends MappingVisitor { private XMLPlannerEnvironment planEnv; public SourceNodePlannerVisitor(XMLPlannerEnvironment planEnv) { this.planEnv = planEnv; } public void visit(MappingSourceNode sourceNode) { try { // create a basic query for the mapping class. String groupName = sourceNode.getResultName(); GroupSymbol groupSymbol = null; GroupSymbol newGroupSymbol = null; try { groupSymbol = QueryUtil.createResolvedGroup(groupName, planEnv.getGlobalMetadata()); newGroupSymbol = createAlternateGroup(groupSymbol, sourceNode); } catch (QueryMetadataException e) { /* * JIRA JBEDSP-531 if a node is excluded and has no transformation, then it matches the * designer notion of an incomplete document. we'll allow this by removing everything * starting at this point */ MappingNode current = sourceNode; boolean isExcluded = false; while (current != null) { if (current.isExcluded()) { isExcluded = true; break; } current = current.getParent(); } if (!isExcluded) { throw e; } //cut me and everything below me from the tree sourceNode.getParent().getChildren().remove(sourceNode); sourceNode.getChildren().clear(); return; } String newGroup = newGroupSymbol.getName(); ResultSetInfo rsInfo = sourceNode.getResultSetInfo(); //create the command off of the unresolved group symbol Query baseQuery = QueryUtil.wrapQuery(new UnaryFromClause(new GroupSymbol(newGroup)), newGroup); baseQuery.getSelect().clearSymbols(); for (Iterator<ElementSymbol> i = ResolverUtil.resolveElementsInGroup(groupSymbol, planEnv.getGlobalMetadata()).iterator(); i.hasNext();) { ElementSymbol ses = i.next(); baseQuery.getSelect().addSymbol(new ElementSymbol(newGroup + Symbol.SEPARATOR + ses.getShortName())); } rsInfo.setCommand(baseQuery); QueryNode modifiedNode = QueryUtil.getQueryNode(newGroup, planEnv.getGlobalMetadata()); Command command = QueryUtil.getQuery(newGroup, modifiedNode, planEnv); MappingSourceNode parent = sourceNode.getParentSourceNode(); Collection<ElementSymbol> bindings = QueryUtil.getBindingElements(modifiedNode); rsInfo.setInputSet(!bindings.isEmpty()); // root source nodes do not have any inputset criteria on them; so there is no use in // going through the raising the criteria. // if the original query is not a select.. we are out of luck. we can expand on this later // versions. make sure bindings are only to parent. if (!rsInfo.hasInputSet() || !canRaiseInputset(command, bindings) || !areBindingsOnlyToNode(modifiedNode, parent)) { return; } // now get the criteria set at the design time; and walk and remove any inputset // criteria. Query transformationQuery = (Query)command; Criteria criteria = transformationQuery.getCriteria(); Criteria nonInputsetCriteria = null; Criteria inputSetCriteria = null; for (Iterator<Criteria> i = Criteria.separateCriteriaByAnd(criteria).iterator(); i.hasNext();) { Criteria conjunct = i.next(); // collect references in the criteria; if there are references; then this is // set by inputset criteria Collection<ElementSymbol> references = QueryUtil.getBindingsReferences(conjunct, bindings); if (references.isEmpty()) { nonInputsetCriteria = Criteria.combineCriteria(nonInputsetCriteria, conjunct); } else { inputSetCriteria = Criteria.combineCriteria(inputSetCriteria, conjunct); } } if (inputSetCriteria == null) { return; } // Keep the criteria which is not reference based. transformationQuery.setCriteria(nonInputsetCriteria); // check and map/convert the inputset criteria elements to groupName, so that // this criteria mapped on the baseQuery; boolean addedProjectedSymbol = convertCriteria(newGroupSymbol, transformationQuery, inputSetCriteria, planEnv.getGlobalMetadata(), sourceNode.getSymbolMap()); if (addedProjectedSymbol && transformationQuery.getSelect().isDistinct()) { transformationQuery.getSelect().setDistinct(false); baseQuery.getSelect().setDistinct(true); } String inlineViewName = planEnv.getAliasName(newGroup); transformationQuery = QueryUtil.wrapQuery(new SubqueryFromClause(inlineViewName, transformationQuery), inlineViewName); // Now that we have the modified Query Node for the group name // we need to update the metadata. QueryNode relationalNode = new QueryNode(SQLStringVisitor.getSQLString(transformationQuery)); planEnv.addQueryNodeToMetadata(newGroupSymbol.getMetadataID(), relationalNode); QueryUtil.markBindingsAsNonExternal(inputSetCriteria, bindings); baseQuery.setCriteria(inputSetCriteria); rsInfo.setCriteriaRaised(true); } catch (Exception e) { throw new TeiidRuntimeException(e); } } /** * check to make sure that given query nodes bindings are only to the node provided, * if they are returns true; false otherwise */ private boolean areBindingsOnlyToNode(QueryNode modifiedNode, MappingSourceNode sourceNode) throws TeiidComponentException { List<Expression> bindings = QueryResolver.parseBindings(modifiedNode); String nodeStr = sourceNode.getActualResultSetName() + Symbol.SEPARATOR; for (Iterator<Expression> i = bindings.iterator(); i.hasNext();) { Expression ses = i.next(); if (ses instanceof AliasSymbol) { ses = ((AliasSymbol)ses).getSymbol(); } ElementSymbol binding = (ElementSymbol)ses; if (!binding.getName().startsWith(nodeStr)) { return false; } } return true; } static String getNewName(String groupName, TempMetadataStore store) { int index = 1; String newGroup = null; while (true) { newGroup = (groupName + "_" + index++).toUpperCase(); //$NON-NLS-1$ if (!store.getData().containsKey(newGroup)) { break; } } return newGroup; } /** * In mapping document, sometimes sibiling nodes might share the mapping class defination, * however during the runtime depending upon where they are used their criteria may be * different on different path of execution (choice nodes), so here we alias all the result * set names in a mapping document. */ private GroupSymbol createAlternateGroup(GroupSymbol oldSymbol, MappingSourceNode sourceNode) throws QueryMetadataException, TeiidComponentException, QueryPlannerException { // get elements in the old group List<ElementSymbol> elements = ResolverUtil.resolveElementsInGroup(oldSymbol, planEnv.getGlobalMetadata()); TempMetadataStore store = planEnv.getGlobalMetadata().getMetadataStore(); // create a new group name and to the temp store String newGroup = getNewName(oldSymbol.getName(), store); GroupSymbol newGroupSymbol = new GroupSymbol(newGroup); newGroupSymbol.setMetadataID(store.addTempGroup(newGroup, elements)); // create a symbol map; so that all the others who refer by the old name can use this map // to convert to new group. sourceNode.setSymbolMap(QueryUtil.createSymbolMap(oldSymbol, newGroup, elements)); // now that we created a new group; now define the query node for this new group based // on the old one. QueryNode oldQueryNode = QueryUtil.getQueryNode(oldSymbol.getName(), planEnv.getGlobalMetadata()); // move the query and its bindings QueryNode modifiedNode = new QueryNode(oldQueryNode.getQuery()); mapBindings(sourceNode, oldQueryNode, modifiedNode); // add the query node for the new group into metadata. planEnv.addQueryNodeToMetadata(newGroupSymbol.getMetadataID(), modifiedNode); return newGroupSymbol; } static void mapBindings(MappingSourceNode sourceNode, QueryNode oldQueryNode, QueryNode modifiedNode) throws TeiidComponentException { if (oldQueryNode.getBindings() != null) { List<String> bindings = new ArrayList<String>(); for (Iterator<Expression> i = QueryResolver.parseBindings(oldQueryNode).iterator(); i.hasNext();) { Expression ses = i.next(); String name = Symbol.getName(ses); boolean useName = false; if (ses instanceof AliasSymbol) { ses = ((AliasSymbol)ses).getSymbol(); useName = true; } ElementSymbol es = (ElementSymbol)ses; if (!useName) { bindings.add(sourceNode.getMappedSymbol(es).toString()); } else { bindings.add(new AliasSymbol(name, sourceNode.getMappedSymbol(es)).toString()); } } modifiedNode.setBindings(bindings); } } private boolean canRaiseInputset(Command command, Collection<ElementSymbol> bindings) { // check to see if this is query. if (!(command instanceof Query)) { return false; } Query query = (Query)command; Criteria crit = query.getCriteria(); if (crit != null && (query.getGroupBy() != null || query.getHaving() != null || query.getLimit() != null)) { return false; } //temporarily remove the criteria query.setCriteria(null); //just throw away order by query.setOrderBy(null); List<ElementSymbol> references = QueryUtil.getBindingsReferences(query, bindings); query.setCriteria(crit); //if there are any input set bindings in the rest of the command, don't convert return references.isEmpty(); } /** * Convert the critria group elements from its native group to supplied group. * @param newGroupSymbol - group to which the criterial elements to be modified * @param criteria - criteria on which the elements need to modified * @return true if converted; false otherwise */ private boolean convertCriteria(GroupSymbol newGroupSymbol, Query transformationQuery, Criteria criteria, TempMetadataAdapter metadata, Map symbolMap) { String groupName = newGroupSymbol.getName(); Collection<ElementSymbol> elementsInCriteria = ElementCollectorVisitor.getElements(criteria, true); Map<ElementSymbol, ElementSymbol> mappedElements = new HashMap<ElementSymbol, ElementSymbol>(); List<Expression> projectedSymbols = transformationQuery.getProjectedSymbols(); boolean addedProjectedSymbol = false; for (Iterator<ElementSymbol> i = elementsInCriteria.iterator(); i.hasNext();) { final ElementSymbol symbol = i.next(); if (symbol.isExternalReference()) { continue; } if (projectedSymbols.contains(symbol)) { mappedElements.put(symbol, new ElementSymbol(groupName + Symbol.SEPARATOR + symbol.getShortName())); continue; } AliasSymbol alias = getMachingAlias(projectedSymbols, symbol); if (alias != null) { mappedElements.put(symbol, new ElementSymbol(groupName + Symbol.SEPARATOR + alias.getShortName())); continue; } // this means that the criteria symbol, is not projected, so add the element symbol // to query node, so that it is projected. String name = getNewSymbolName(newGroupSymbol.getName(), symbol, symbolMap); AliasSymbol selectSymbol = new AliasSymbol(name, symbol); transformationQuery.getSelect().addSymbol(selectSymbol); addedProjectedSymbol = true; // also add to the projected elements on the temp group. metadata.getMetadataStore().addElementSymbolToTempGroup(newGroupSymbol.getName(), selectSymbol); ElementSymbol upperSymbol = new ElementSymbol(groupName + Symbol.SEPARATOR + selectSymbol.getShortName()); mappedElements.put(symbol, upperSymbol); //add to the symbol map. the base symbol is not to the original group, since it doesn't really project this element symbolMap.put(upperSymbol, upperSymbol); } // now that we have resolved criteria elements; map them correctly ExpressionMappingVisitor.mapExpressions(criteria, mappedElements); return addedProjectedSymbol; } static String getNewSymbolName(String newGroupName, ElementSymbol elementSymbol, Map symbolMap) { int index = 1; String newSymbolName = elementSymbol.getShortName(); while (symbolMap.values().contains(new ElementSymbol(newGroupName + Symbol.SEPARATOR + newSymbolName))) { newSymbolName = elementSymbol.getShortName() + "_" + index++; //$NON-NLS-1$ } return newSymbolName; } /** * If the element has alias wrapping, then return the matching alias element. * @return matched alias symbol; null otherwise. */ private AliasSymbol getMachingAlias(List<Expression> elementsInGroup, ElementSymbol symbol) { for (Expression element : elementsInGroup) { if (element instanceof AliasSymbol) { AliasSymbol alias = (AliasSymbol)element; if (alias.getSymbol().equals(symbol)) { return alias; } } } return null; } /** * try to split the criteria based on if that is inputset criteria or not. * @param doc * @param planEnv * @return */ public static MappingDocument raiseInputSet(MappingDocument doc, XMLPlannerEnvironment planEnv) throws QueryPlannerException, QueryMetadataException, TeiidComponentException { SourceNodePlannerVisitor real = new SourceNodePlannerVisitor(planEnv); planWalk(doc, real); return doc; } private static void planWalk(MappingDocument doc, MappingVisitor visitor) throws QueryPlannerException, QueryMetadataException, TeiidComponentException { try { Navigator walker = new Navigator(true, visitor) { /* * Special walking of children so that we can safely remove nodes */ @Override protected void walkChildNodes(MappingNode element) { List<MappingNode> children = new ArrayList<MappingNode>(element.getNodeChildren()); for(Iterator<MappingNode> i=children.iterator(); i.hasNext();) { if (shouldAbort()) { break; } MappingNode node = i.next(); node.acceptVisitor(this); } } }; doc.acceptVisitor(walker); } catch (TeiidRuntimeException e) { if (e.getCause() instanceof QueryPlannerException) { throw (QueryPlannerException)e.getCause(); } else if (e.getCause() instanceof QueryMetadataException) { throw (QueryMetadataException)e.getCause(); } else if (e.getCause() instanceof TeiidComponentException) { throw (TeiidComponentException)e.getCause(); } else { throw e; } } } }