/* * JBoss, Home of Professional Open Source. * * See the LEGAL.txt file distributed with this work for information regarding copyright ownership and licensing. * * See the AUTHORS.txt file distributed with this work for a full listing of individual contributors. */ package org.teiid.query.ui.sqleditor.component; import java.util.ArrayList; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.teiid.designer.core.ModelerCore; import org.teiid.designer.query.sql.ISQLStringVisitor; import org.teiid.designer.query.sql.IToken; import org.teiid.designer.query.sql.lang.ICriteria; import org.teiid.designer.query.sql.lang.IExpression; import org.teiid.designer.query.sql.lang.ILanguageObject; /** * The <code>DisplayNode</code> class is the base class used by <code>QueryDisplayComponent</code> to represent all types of * Display Nodes. * * @since 8.0 */ public class DisplayNode implements DisplayNodeConstants { // ///////////////////////////////////////////////////////////////////////// // FIELDS // ///////////////////////////////////////////////////////////////////////// protected int startIndex = 0; protected int endIndex = 0; protected DisplayNode parentNode = null; protected ILanguageObject languageObject = null; protected List<DisplayNode> childNodeList = new ArrayList(1); protected List<DisplayNode> displayNodeList = new ArrayList(1); protected List<CommentDisplayNode> commentNodeList = new ArrayList<CommentDisplayNode>(1); private boolean visible = true; protected DisplayNode() { } // ///////////////////////////////////////////////////////////////////////// // PUBLIC METHODS // ///////////////////////////////////////////////////////////////////////// /** * Get the Child Nodes of this Display Node */ public DisplayNode getParent() { return parentNode; } /** * Get the LanguageObject associated with this DisplayNode */ public ILanguageObject getLanguageObject() { return languageObject; } /** * Get the Child Nodes of this Display Node */ public List<DisplayNode> getChildren() { return childNodeList; } /** * Returns a flattened display node list of the entire tree under this Node. */ public List getDisplayNodeList() { return displayNodeList; } /** * @return all the comments surrounding this node tree */ public List<CommentDisplayNode> getCommentNodeList() { return commentNodeList; } /** * @return True if this display node is visible within it's containing UI component. * @since 5.0.1 */ public boolean isVisible() { return this.visible; } /** * @param visible <code>true</code> if this display node is visible within it's containing UI component. * @param includeDescendents <code>true</code> if the visibility of this node's descendents should also be affected. * @since 5.0.1 */ public void setVisible( boolean visible, boolean includeDescendents ) { this.visible = visible; if (includeDescendents && this.childNodeList != null) { for (Iterator iter = this.childNodeList.iterator(); iter.hasNext();) { ((DisplayNode)iter.next()).setVisible(visible, includeDescendents); } // for } if (this.displayNodeList != null) { for (Iterator iter = this.displayNodeList.iterator(); iter.hasNext();) { DisplayNode node = (DisplayNode)iter.next(); if (node.parentNode == this) { node.setVisible(visible, includeDescendents); } } // for } } private String escape(String text, String character, boolean optional) { String target = ESCAPE + character; String replacement = REGEX_ESCAPE + character + (optional ? QMARK : BLANK); text = text.replaceAll(target, replacement); return text; } private int nextAvailableSpace(String sql, int index) { for (int i = index - 1; i < sql.length(); ++i) { char c = sql.charAt(i); if (System.lineSeparator().equals(Character.toString(c))) { // Character is a newline return i + 1; } } return sql.length(); } private int calculateLocation(String sql, CommentDisplayNode comment) { int cmtIdx = comment.getOffset(); LinkedList<IToken> preTokens = new LinkedList(comment.getPreTokens()); if (preTokens.isEmpty()) return cmtIdx; // can happen if offset is 0 and comment is at the start // i : case-insensitive mode // s: include line terminators in . matchings StringBuffer regex = new StringBuffer("(?is)"); //$NON-NLS-1$ Iterator<IToken> iterator = preTokens.iterator(); while(iterator.hasNext()) { IToken token = iterator.next(); ISQLStringVisitor visitor = ModelerCore.getTeiidQueryService().getSQLStringVisitor(); String text = visitor.displayName(token); // Must escape question marks first since optional is represented by question marks in regex text = escape(text, QMARK, false); text = escape(text, SPEECH_MARK, true); text = escape(text, QUOTE, true); text = escape(text, LTPAREN, true); text = escape(text, RTPAREN, true); // The zero at the end of a time is optional, eg. 19:00:02.50, so // gets dropped by the production. Thus, need to make it // optional. Have to do it prior to escaping DOT, otherwise the // replace regex becomes consumed by backslashes! String target = "([0-9][0-9]:[0-9][0-9]:[0-9][0-9]\\.[0-9])0"; //$NON-NLS-1$ String replacement = "$1[0-9]?'"; //$NON-NLS-1$ text = text.replaceAll(target, replacement); // The source hint is parseable as /*+ sh ... */ but all spaces are removed // between the + and sh upon conversion. target = "\\/\\*\\+\\s*sh"; //$NON-NLS-1$ replacement = "/*+sh"; //$NON-NLS-1$ text = text.replaceAll(target, replacement); text = escape(text, DOT, false); text = escape(text, LTBRACE, false); // Makes right brace optional since fn{ is dropped as the prefix of functions // and its closing brace will remain as a pre token text = escape(text, RTBRACE, true); text = escape(text, FORWARD_SLASH, false); text = escape(text, STAR, false); text = escape(text, PLUS, false); text = escape(text, PIPE, false); regex.append(text); if (iterator.hasNext()) regex.append(DOT).append(STAR).append(QMARK); } Pattern pattern = Pattern.compile(regex.toString()); Matcher matcher = pattern.matcher(sql); // // Find ALL the possible sub sequences that the matcher could match. // Most of the time it should be only one but sometimes a SELECT subquery // could have the same WHERE condition as the outer master query. // WITH x AS (SELECT a FROM db.g WHERE b = aString /* Comment 1 */) // SELECT a FROM db.g WHERE b = aString /* Comment 2 */ List<Integer> results = new ArrayList<Integer>(); boolean result = true; while (result) { result = matcher.find(); if (result) { // We know that the matcher has succeeded once but is possible that // since its a reluctant matcher it is in fact finding a result far earlier // than required. results.add(matcher.end()); } } if (results.isEmpty()) return -1; // // Loop through the candidates and choose the pair {start, end} which has an end // value closest to the offset of the comment // Integer pref = results.get(0); int diff = Math.abs(pref - cmtIdx); for (Integer poss : results) { int pdiff = Math.abs(poss - cmtIdx); if (pdiff < diff) pref = poss; } int offset = pref + 1; if (offset != cmtIdx) cmtIdx = offset; // // At this point, we have a location index for the comment. // However, its still possible, despite best efforts that that index // is within a term. Thus, check the index finally... // return nextAvailableSpace(sql, cmtIdx); } private int findIndentLevel(String sql, int index) { if (index == 0) return 0; // start of sequence has no tabs if (index >= sql.length()) return 0; // end of sequence has no tabs int nextTabs = 0; char prevChar = sql.charAt(index - 1); if (System.lineSeparator().equals(Character.toString(prevChar))) { // The index is at the beginning of the line so the comment will be // inserted as a previous line. In that case, need to find any tabs on // THIS line as the next tabs for (int i = index; i < sql.length(); ++i) { char c = sql.charAt(i); if (System.lineSeparator().equals(Character.toString(c))) break; if ('\t' == c) nextTabs++; } } else { boolean count = false; for (int i = index; i < sql.length() && index > 0; ++i) { char c = sql.charAt(i); // Find the first instance of a new line character first if (System.lineSeparator().equals(Character.toString(c))) { count = true; continue; } if (! count) continue; if ('\t' == c) { nextTabs++; continue; } break; } } // The index position is counted in the 'next' count above // so this needs to use the previous index. However, still // want any trailing comments to be non-tabbed so use // index rather than i to test against sql.length() int prevTabs = 0; for (int i = index - 1; i > 0 && index < sql.length(); --i) { char c = sql.charAt(i); if ('\t' == c) prevTabs++; else if (prevTabs > 0) // found all the prev tabs available break; } return Math.max(nextTabs, prevTabs); } private void addComments(StringBuffer buf) { for (CommentDisplayNode comment : commentNodeList) { String text = buf.toString(); int insertIdx = calculateLocation(text, comment); if (insertIdx == -1) continue; int indentLevel = findIndentLevel(text, insertIdx); // Handling trailing comments if (insertIdx >= text.length()) { // insert index is the end of the sql string if (! text.endsWith(CR)) buf.append(CR); // Add in tabs prior to comment for (int i = 0; i < indentLevel; ++i) buf.append(TAB); buf.append(comment.toDisplayString()); if (! comment.isMultiLine()) buf.append(CR); } else { // Most of the comments with be dealt with here // Add in tabs prior to comment StringBuffer cmt = new StringBuffer(); for (int i = 0; i < indentLevel; ++i) cmt.append(TAB); cmt.append(comment.toDisplayString()); cmt.append(CR); buf.insert(insertIdx, cmt.toString()); } } } private String generateString() { ISQLStringVisitor sqlStringVisitor = ModelerCore.getTeiidQueryService().getSQLStringVisitor(); sqlStringVisitor.disableComments(this); StringBuffer sb = new StringBuffer(); Iterator iter = displayNodeList.iterator(); while (iter.hasNext()) { sb.append(((DisplayNode)iter.next()).toDisplayString()); } addComments(sb); sqlStringVisitor.enableComments(this); return sb.toString(); } /** * @return The displayable String representation for this display node. * @since 5.0.1 */ public String toDisplayString() { return generateString(); } /** * Returns the String representation for this display node. */ @Override public String toString() { return generateString(); } /** * Returns whether the node has any children */ public boolean hasChildren() { return (childNodeList != null && childNodeList.size() > 0) ? true : false; } /** * Returns whether the node has any display nodes */ public boolean hasDisplayNodes() { return (displayNodeList.size() > 0) ? true : false; } /** * Determine if the DisplayNode supports elements in it. Default implementation returns false. */ public boolean supportsElement() { return false; } /** * Determine if the DisplayNode supports groups in it. Default implementation returns false. */ public boolean supportsGroup() { return false; } /** * Determine if the DisplayNode supports expressions in it. */ public boolean supportsExpression() { return isInExpression(); } /** * Determine if this DisplayNode is within an expression. Checks whether the parentNode is an ExpressionDisplayNode */ public boolean isInExpression() { return (getExpression() != null); } /** * Get ExpressionDisplayNode */ public DisplayNode getExpression() { DisplayNode parentNode = this; while (parentNode != null) { if (parentNode.languageObject != null && parentNode.languageObject instanceof IExpression) { return parentNode; } parentNode = parentNode.getParent(); } return null; } /** * Determine if the DisplayNode supports criteria in it. */ public boolean supportsCriteria() { return isInCriteria(); } /** * Determine if the DisplayNode is within a criteria. Checks whether the parentNode is a CriteriaDisplayNode. */ public boolean isInCriteria() { return getCriteria() != null; } /** * Get CriteriaDisplayNode */ public DisplayNode getCriteria() { DisplayNode parentNode = this; while (parentNode != null) { if (parentNode.languageObject instanceof ICriteria) { return parentNode; } parentNode = parentNode.getParent(); } return null; } /** * Sets the starting index for this node and reindex everything under it. */ public int setStartIndex( int index ) { startIndex = index; endIndex = index; // ------------------------------------------ // Reindex the DisplayNodeList // ------------------------------------------ Iterator iter = displayNodeList.iterator(); DisplayNode node = null; if (iter.hasNext()) { node = (DisplayNode)iter.next(); endIndex = node.setStartIndex(endIndex); } startIndex = endIndex + 1; while (iter.hasNext()) { node = (DisplayNode)iter.next(); endIndex = node.setStartIndex(startIndex); startIndex = endIndex + 1; } // --------------------------------------------------- // Reindex all of the display node parents // --------------------------------------------------- iter = displayNodeList.iterator(); while (iter.hasNext()) { DisplayNode displayNode = (DisplayNode)iter.next(); reindexParents(displayNode); } return endIndex; } /** * Reindex the Parents of this display Node */ private void reindexParents( DisplayNode node ) { while (node != null) { DisplayNode parentNode = node.getParent(); // ------------------------------------------------ // If currentNode has Parent, index the Parent // ------------------------------------------------ if (parentNode != null) { List childDisplayNodes = parentNode.getDisplayNodeList(); int nd = childDisplayNodes.size(); if (nd != 0) { parentNode.startIndex = ((DisplayNode)childDisplayNodes.get(0)).getStartIndex(); parentNode.endIndex = ((DisplayNode)childDisplayNodes.get(nd - 1)).getEndIndex(); } } // Reset Node to parent node = parentNode; } } /** * Returns the starting index for this node */ public int getStartIndex() { return startIndex; } /** * Returns the ending index for this node */ public int getEndIndex() { return endIndex; } /** * Returns the length of the node */ public int length() { return endIndex - startIndex + 1; } /** * Returns true if index is at the start of the display node */ public boolean isIndexAtStart( int index ) { return (index != startIndex) ? false : true; } /** * Returns true if index is at the end of the display node */ public boolean isIndexAtEnd( int index ) { return (index != (endIndex + 1)) ? false : true; } /** * Returns true if index is anywhere within the display node, including the start and end position */ public boolean isAnywhereWithin( int index ) { return (index >= startIndex && index <= (endIndex + 1)) ? true : false; } /** * Returns true if index is anywhere within the display node, NOT including the start and end position */ public boolean isWithin( int index ) { return (index > startIndex && index < (endIndex + 1)) ? true : false; } protected void addChildNode( DisplayNode child ) { childNodeList.add(child); displayNodeList.add(child); } /** * @param commentNode */ public void addCommentNode(CommentDisplayNode commentNode) { commentNodeList.add(commentNode); } }