/* * 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.designer.jdbc.metadata.impl; import java.sql.SQLException; import java.text.CharacterIterator; import java.text.StringCharacterIterator; import java.util.Comparator; import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.Path; import org.teiid.core.designer.util.CoreArgCheck; import org.teiid.core.designer.util.IPathComparator; import org.teiid.core.designer.HashCodeUtil; import org.teiid.designer.jdbc.JdbcException; import org.teiid.designer.jdbc.JdbcPlugin; import org.teiid.designer.jdbc.data.Request; import org.teiid.designer.jdbc.metadata.JdbcCatalog; import org.teiid.designer.jdbc.metadata.JdbcDatabase; import org.teiid.designer.jdbc.metadata.JdbcNode; import org.teiid.designer.jdbc.metadata.JdbcNodeVisitor; import org.teiid.designer.jdbc.metadata.JdbcSchema; /** * JdbcNodeImpl * * @since 8.0 */ public abstract class JdbcNodeImpl implements JdbcNode, Comparable, InternalJdbcNode { public static final String EXCLUDED_PATTERN = null; public static final String WILDCARD_PATTERN = "%"; //$NON-NLS-1$ public static final String NOT_APPLICABLE = ""; //$NON-NLS-1$ public static final String DEFAULT_QUALIFIED_NAME_DELIMITER = "."; //$NON-NLS-1$ /** Used as the value for {@link #getChildren()} when there are no children */ private static final JdbcNode[] EMPTY_CHILDREN_ARRAY = new JdbcNodeImpl[] {}; private IPath path; private final int type; private final String name; private final JdbcNode parent; private JdbcNode[] children; private Object childrenLock = new Object(); private RequestContainer requests; private int selectionMode; private String qualifiedNameDelimiter; /** * Construct an instance of JdbcNodeImpl with the type, name and parent information. * * @param type the type for this node * @param name the name for this node; may be null * @param parent the parent node; may be null if the node is to be a root node */ protected JdbcNodeImpl( int type, String name, JdbcNode parent ) { super(); this.type = type; // Remove path information from name (occurs for MS Access) if (name == null) { this.name = ""; //$NON-NLS-1$ } else { name = name.substring(name.lastIndexOf('/') + 1); name = name.substring(name.lastIndexOf('\\') + 1); this.name = name; } this.parent = parent; this.children = null; // Set the path ... if (this.parent == null) { this.path = Path.ROOT; } else { this.path = this.parent.getPath().append(getName()); } // Set the selection mode using the parent information ... this.selectionMode = getDefaultSelectionMode(); if (parent != null) { final int parentSelectionMode = parent.getSelectionMode(); if (parentSelectionMode == SELECTED) { doSetSelectionMode(SELECTED); } else if (parentSelectionMode == UNSELECTED) { doSetSelectionMode(UNSELECTED); } else { // parent mode is ambiguous, so look in the JdbcDatabase's list of selections ... final JdbcNodeSelections selections = ((InternalJdbcDatabase)this.getJdbcDatabase()).getJdbcNodeSelections(); final int mode = selections.getSelectionMode(this.path); if (mode == JdbcNodeSelections.SELECTED) { this.selectionMode = SELECTED; } else if (mode == JdbcNodeSelections.UNSELECTED) { this.selectionMode = UNSELECTED; } else if (mode == JdbcNodeSelections.PARTIALLY_SELECTED) { this.selectionMode = PARTIALLY_SELECTED; } } } } /* (non-Javadoc) * @See org.teiid.designer.jdbc.metadata.JdbcNode#getPath() */ @Override public IPath getPath() { return this.path; } /* (non-Javadoc) * @See org.teiid.designer.jdbc.metadata.JdbcNode#getPathInSource() */ @Override public abstract IPath getPathInSource(); /** * This method implementation returns true by default. * * @see org.teiid.designer.jdbc.metadata.JdbcNode#isDatabaseObject() */ @Override public boolean isDatabaseObject() { return true; } /* (non-Javadoc) * @See org.teiid.designer.jdbc.metadata.JdbcNode#getParentDatabaseObject(boolean, boolean) */ @Override public abstract JdbcNode getParentDatabaseObject( boolean includeCatalog, boolean includeSchema ); /* (non-Javadoc) * @See org.teiid.designer.jdbc.metadata.JdbcNode#findChild(java.lang.String) */ @Override public JdbcNode findChild( String name ) { final JdbcDatabase dbNode = getJdbcDatabase(); CoreArgCheck.isNotNull(dbNode); return dbNode.findJdbcNode(this.getPath().append(name)); } /* (non-Javadoc) * @See org.teiid.designer.jdbc.metadata.JdbcNode#getName() */ @Override public String getName() { return this.name; } /* (non-Javadoc) * @See org.teiid.designer.jdbc.metadata.JdbcNode#getParent() */ @Override public JdbcNode getParent() { return this.parent; } /* (non-Javadoc) * @See org.teiid.designer.jdbc.metadata.JdbcNode#getType() */ @Override public int getType() { return this.type; } /** * By default, this implementation returns true. Subclasses should override this method if they are considered leaf nodes and * never have children. * * @see org.teiid.designer.jdbc.metadata.JdbcNode#allowsChildren() */ @Override public boolean allowsChildren() { return true; } /* (non-Javadoc) * @See org.teiid.designer.jdbc.metadata.JdbcNode#getChildren() */ @Override public JdbcNode[] getChildren() throws JdbcException { // Check if null; this is a check that works fast if non-null if (children == null) { // If null, then obtain the lock synchronized (childrenLock) { // And check again in case some other thread populated the children // while this thread was waiting for the lock in the previous line. if (children == null) { // Compute the children for this node children = computeChildren(); // may return null, may throw exception // If null, the set to the empty array if (children == null) { children = EMPTY_CHILDREN_ARRAY; // Nothing to register } else { // Register the children in the database cache final InternalJdbcDatabase dbNode = (InternalJdbcDatabase)getJdbcDatabase(); final JdbcNodeCache cache = dbNode.getJdbcNodeCache(); for (int i = 0; i < children.length; ++i) { cache.put(children[i]); } } } } } return children; } /** * Utility method (mostly for testing) that allows one to add a child node one at a time. This method does <i>not</i> check * whether the node already exists as a child of this node. * * Warning: this method is only made public to allow for unit testing. Other uses are discouraged. */ public void addChild( final JdbcNode node ) throws JdbcException { final JdbcNode[] currentChildren = this.getChildren(); // may throw exception final int currentNumChildren = currentChildren.length; if (currentNumChildren != 0) { final JdbcNode[] newChildren = new JdbcNode[currentNumChildren + 1]; System.arraycopy(currentChildren, 0, newChildren, 0, currentNumChildren); newChildren[currentNumChildren] = node; children = newChildren; } else { children = new JdbcNode[] {node}; } // Register the new node ... final InternalJdbcDatabase dbNode = (InternalJdbcDatabase)getJdbcDatabase(); dbNode.getJdbcNodeCache().put(node); } /** * Refresh this node by clearing any cached information, including {@link #getChildren() children}. */ @Override public void refresh() { if (children != null) { synchronized (childrenLock) { // Remove existing children from the cache and call refresh on them ... final InternalJdbcDatabase dbNode = (InternalJdbcDatabase)getJdbcDatabase(); final JdbcNodeCache cache = dbNode.getJdbcNodeCache(); for (int i = 0; i < children.length; ++i) { final JdbcNode child = children[i]; cache.remove(child); child.refresh(); } children = null; } } } /** * Compute the children for this node. This method is called the first time the children are needed (i.e., when the * {@link #getChildren()} method is called), and is called from within a synchronized method. * * @return the array of child objects for this node; may be null if there are no children * @throws JdbcException if there is an error obtaining the children for this node */ protected abstract JdbcNode[] computeChildren() throws JdbcException; /** * Return the stringified form of this node. By default, the string is of the form "<i>{@link #getTypeName() typeName} * {@link #getName() name}</i>". However, subclasses may override this behavior. * * @return the string form of this node */ @Override public String toString() { final String typeName = getTypeName(); return (typeName == null ? "" : typeName) + name; //$NON-NLS-1$ } /* (non-Javadoc) * @see java.lang.Object#equals(java.lang.Object) */ @Override public boolean equals( Object obj ) { // Check if instances are identical ... if (this == obj) { return true; } // Check if object can be compared to this one // (this includes checking for null ) ... // if ( this.getClass().isInstance(obj) ) { if (obj instanceof JdbcNodeImpl) { final JdbcNodeImpl that = (JdbcNodeImpl)obj; // Check that the types are identical if (this.type != that.type) { return false; } // Check that the parent is the same if (this.parent != that.parent) { return false; } // Check that the names match (case INsensitive) if (this.name != null) { if (!this.name.equalsIgnoreCase(that.name)) { return false; } } else { if (that.name != null) { // this.name is null return false; } // else this.name == that.name == null } } // Otherwise not comparable ... return false; } /* (non-Javadoc) * @see java.lang.Object#hashCode() */ @Override public int hashCode() { int hash = super.hashCode(); hash = HashCodeUtil.hashCode(hash, this.type); hash = HashCodeUtil.hashCode(hash, this.parent); hash = HashCodeUtil.hashCode(hash, this.name); return hash; } /** * Compares this object to another. If the specified object is not an instance of the JdbcNodeImpl class, then this method * throws a ClassCastException (as instances are comparable only to instances of the same class). Note: this method <i>is</i> * consistent with <code>equals()</code>, meaning that <code>(compare(x, y)==0) == (x.equals(y))</code>. * <p> * * @param obj the object that this instance is to be compared to * @return a negative integer, zero, or a positive integer as this object is less than, equal to, or greater than the * specified object, respectively. * @throws ClassCastException if the specified object's type prevents it from being compared to this instance. */ @Override public int compareTo( Object obj ) { if (obj == null) { return 1; // this is > null } final JdbcNodeImpl that = (JdbcNodeImpl)obj; // May throw ClassCastException CoreArgCheck.isNotNull(obj); // Check that the types are identical final int diffType = this.type - that.type; if (diffType != 0) { return diffType; } // Compare the paths ... final Comparator comparator = new IPathComparator(); return comparator.compare(this.path, that.path); } /* (non-Javadoc) * @See org.teiid.designer.jdbc.metadata.JdbcNode#accept(org.teiid.designer.jdbc.metadata.JdbcNodeVisitor, int) */ @Override public void accept( final JdbcNodeVisitor visitor, final int depth ) throws JdbcException { CoreArgCheck.isNotNull(visitor); CoreArgCheck.isTrue(depth == DEPTH_INFINITE || depth == DEPTH_ONE || depth == DEPTH_ZERO, JdbcPlugin.Util.getString("JdbcNodeImpl.InvalidDepthValue")); //$NON-NLS-1$ // visit this resource if (!visitor.visit(this) || depth == DEPTH_ZERO) return; // visit the children final int nextDepth = (depth == DEPTH_INFINITE ? DEPTH_INFINITE : DEPTH_ZERO); final JdbcNode[] children = this.getChildren(); for (int i = 0; i < children.length; ++i) { children[i].accept(visitor, nextDepth); } } /** * Utility method to return the name of the catalog in which this node exists. If this node does not exist in a catalog, this * method returns the {@link #EXCLUDED_PATTERN} constant. * * @return the catalog name, or EXCLUDED_PATTERN if <code>node</code> doesn't exist in a catalog. */ public static String getCatalogName( final JdbcNode node ) { final JdbcDatabase dbNode = node.getJdbcDatabase(); JdbcNode ancestor = node.getParent(); // Stop when there is no ancestor or the ancestor is the database node while (ancestor != null && ancestor != dbNode) { // Then see if CatalogNode if (ancestor instanceof JdbcCatalog) { return ancestor.getName(); } // Otherwise, keep going up ancestor = ancestor.getParent(); } return EXCLUDED_PATTERN; } /** * Utility method to return the name of the catalog in which this node exists. If this node does not exist in a catalog, this * method returns the {@link #NOT_APPLICABLE} constant. * * @return the catalog name, or NOT_APPLICABLE if <code>node</code> doesn't exist in a catalog. */ public static String getSchemaName( final JdbcNode node ) { final JdbcDatabase dbNode = node.getJdbcDatabase(); JdbcNode ancestor = node.getParent(); // Stop when there is no ancestor or the ancestor is the database node while (ancestor != null && ancestor != dbNode) { // Then see if JdbcSchema if (ancestor instanceof JdbcSchema) { return ancestor.getName(); } // Otherwise, keep going up ancestor = ancestor.getParent(); } return EXCLUDED_PATTERN; } /** * Utility method to return the name of the catalog in which this node exists. If this node does not exist in a catalog, this * method returns the {@link #NOT_APPLICABLE} constant. However, if this database does not support schemas, this method * returns null. * * @return the catalog name pattern, NOT_APPLICABLE if <code>node</code> doesn't exist in a catalog, or null if catalogs are * not supported */ public static String getCatalogPattern( final JdbcNode node ) { String catalogNamePattern = JdbcNodeImpl.getCatalogName(node); if (NOT_APPLICABLE.equals(catalogNamePattern)) { // See if catalogs are even supported ... boolean catalogsSupported = false; try { catalogsSupported = node.getJdbcDatabase().getCapabilities().supportsCatalogsInDataManipulation(); } catch (JdbcException e) { JdbcPlugin.Util.log(e); // not expected, but log just in case } catch (SQLException e) { // ignore; } if (!catalogsSupported) { catalogNamePattern = EXCLUDED_PATTERN; } } return catalogNamePattern; } /** * Utility method to return the name of the schema in which this node exists. If this node does not exist in a schema, this * method returns the {@link #NOT_APPLICABLE} constant. However, if this database does not support schemas, this method * returns null. * * @return the schema name pattern, NOT_APPLICABLE if <code>node</code> doesn't exist in a schema, or null if schemas are not * supported */ public static String getSchemaPattern( final JdbcNode node ) { String schemaNamePattern = JdbcNodeImpl.getSchemaName(node); if (NOT_APPLICABLE.equals(schemaNamePattern)) { // See if catalogs are even supported ... boolean schemaSupported = false; try { schemaSupported = node.getJdbcDatabase().getCapabilities().supportsCatalogsInDataManipulation(); } catch (JdbcException e) { JdbcPlugin.Util.log(e); // not expected, but log just in case } catch (SQLException e) { // ignore; } if (!schemaSupported) { schemaNamePattern = EXCLUDED_PATTERN; } } return schemaNamePattern; } /* (non-Javadoc) * @See org.teiid.designer.jdbc.metadata.JdbcTable#getNamesOfResults() */ @Override public String[] getNamesOfResults() { return getRequestContainer().getNamesOfResults(); } /* (non-Javadoc) * @See org.teiid.designer.jdbc.metadata.JdbcTable#getResults(java.lang.String) */ @Override public Request getRequest( String name ) { return getRequestContainer().getRequest(name); } /* (non-Javadoc) * @See org.teiid.designer.jdbc.metadata.JdbcTable#getResults(java.lang.String) */ @Override public Request getRequest( String name, final boolean includeMetadata ) { return getRequestContainer().getRequest(name, includeMetadata); } protected synchronized RequestContainer getRequestContainer() { if (requests == null) { requests = new RequestContainer(createRequests()); } return requests; } /** * Override this method to add requests to the node. * * @return * @throws JdbcException */ protected Request[] createRequests() { return new Request[] {}; } /* (non-Javadoc) * @See org.teiid.designer.jdbc.metadata.JdbcNode#getSelectionMode() */ @Override public int getSelectionMode() { return this.selectionMode; } /* (non-Javadoc) * @See org.teiid.designer.jdbc.metadata.JdbcNode#setSelectionMode(int) */ @Override public void setSelected( final boolean selected ) { final int newMode = (selected ? SELECTED : UNSELECTED); if (this.selectionMode == newMode) { // The value is the same, so simply return return; } // Set the current mode ... doSetSelectionMode(newMode); // ------------------------------------------------------------------------- // Update the children ... // ------------------------------------------------------------------------- // If the children haven't been loaded, simply return ... if (this.children != null) { // Process the children ... JdbcNode[] childrenCopy = null; synchronized (childrenLock) { // Get the children ... final int numChildren = this.children.length; childrenCopy = new JdbcNode[numChildren]; System.arraycopy(this.children, 0, childrenCopy, 0, numChildren); } // If unselecting ... if (this.selectionMode == UNSELECTED) { // Go through all the children and unselect them ... for (int i = 0; i < childrenCopy.length; ++i) { final JdbcNode child = childrenCopy[i]; child.setSelected(false); } } else { // if ( this.selectionMode == SELECTED ) // Go through all the children and select them ... for (int i = 0; i < childrenCopy.length; ++i) { final JdbcNode child = childrenCopy[i]; child.setSelected(true); } } } // ------------------------------------------------------------------------- // Update the parent ... // ------------------------------------------------------------------------- // This may cause the parent (or its ancestors) to each evaluate all of their children // If there is a parent ... if (this.parent != null && this.parent instanceof InternalJdbcNode) { ((InternalJdbcNode)this.parent).checkSelectionMode(this); } } /** * Return the default selection mode when the selection mode can't be determined any other way. For example, this method is * called when the parent selection mode is {@link JdbcNode#PARTIALLY_SELECTED}. This method returns * {@link JdbcNode#UNSELECTED} by default, and should be overridden by subclasses that wish to provide an alternative. * * @return the default selection mode */ protected int getDefaultSelectionMode() { return UNSELECTED; } protected void doSetSelectionMode( final int mode ) { this.selectionMode = mode; final InternalJdbcDatabase db = (InternalJdbcDatabase)this.getJdbcDatabase(); final JdbcNodeSelections selections = db.getJdbcNodeSelections(); selections.setSelected(this.getPath(), this.selectionMode); } /* (non-Javadoc) * @See org.teiid.designer.jdbc.metadata.impl.InternalJdbcNode#checkSelectionMode(org.teiid.designer.jdbc.metadata.JdbcNode) */ @Override public void checkSelectionMode( final JdbcNode childNodeWithChangedSelection ) { // If the current mode is that of the child ... if (this.selectionMode == childNodeWithChangedSelection.getSelectionMode()) { return; } // Otherwise, we always have to evaluate all children! // If the children haven't been loaded, then do nothing if (this.children == null) { // pathological case that should theoretically never happen return; } // Process the children ... JdbcNode[] childrenCopy = null; synchronized (childrenLock) { // Get the children ... final int numChildren = this.children.length; childrenCopy = new JdbcNode[numChildren]; System.arraycopy(this.children, 0, childrenCopy, 0, numChildren); } // Go through all the children and see what their mode is ... final int previousMode = this.selectionMode; boolean hasUnselected = false; boolean hasSelected = false; boolean hasPartiallySelected = false; for (int i = 0; i < childrenCopy.length; ++i) { final JdbcNode child = childrenCopy[i]; final int childMode = child.getSelectionMode(); if (!hasSelected && childMode == SELECTED) { hasSelected = true; } if (!hasUnselected && childMode == UNSELECTED) { hasUnselected = true; } if (!hasPartiallySelected && childMode == PARTIALLY_SELECTED) { hasPartiallySelected = true; } // See if we know enough to set this node ... if (hasPartiallySelected || (hasSelected && hasUnselected)) { // A child is partially selected, or there are both selected & unselected children ... hasPartiallySelected = true; doSetSelectionMode(PARTIALLY_SELECTED); break; } } // We're through all the children, so they are all either SELECTED or UNSELECTED if (!hasPartiallySelected) { if (hasSelected) { doSetSelectionMode(SELECTED); } if (hasUnselected) { doSetSelectionMode(UNSELECTED); } } // If the value changed ... if (this.selectionMode != previousMode) { // call this method on the parent ... if (this.parent != null && this.parent instanceof InternalJdbcNode) { ((InternalJdbcNode)this.parent).checkSelectionMode(this); } } } /** * @see org.teiid.designer.jdbc.metadata.JdbcNode#getUnqualifiedName() */ @Override public String getUnqualifiedName() { return getUnqualifiedName(getName()); } /** * @see org.teiid.designer.jdbc.metadata.JdbcNode#getUnqualifiedName(java.lang.String) */ @Override public String getUnqualifiedName( final String originalName ) { // Get the identifier quote string ... String quoteString = null; try { quoteString = this.getJdbcDatabase().getCapabilities().getIdentifierQuoteString(); } catch (JdbcException e) { JdbcPlugin.Util.log(e); // not expected, but log just in case } catch (SQLException e) { // ignore; } if (quoteString == null || quoteString.trim().length() == 0) { return originalName; } // // See if the name even needs the quote string ... // boolean extraCharsUsed = true; // assume they are ... // try { // final String extraChars = this.getJdbcDatabase().getCapabilities().getExtraNameCharacters(); // if (extraChars != null && extraChars.length() != 0) { // extraCharsUsed = containsCharacters(originalName, extraChars); // } // } catch (JdbcException e) { // JdbcPlugin.Util.log(e); // not expected, but log just in case // } catch (SQLException e) { // // ignore; // } // if (!extraCharsUsed && isValidName(originalName)) { // // Case 3263: Regardless of result returned above, we should always consider // // name with spaces as needing to be quoted. // if (originalName.indexOf(" ") == -1) { //$NON-NLS-1$ // return originalName; // } // } final StringBuffer sb = new StringBuffer(); sb.append(quoteString); sb.append(originalName); sb.append(quoteString); return sb.toString(); } protected boolean containsCharacters( final String name, final String extraChars ) { final int numChars = extraChars.length(); for (int i = 0; i < numChars; ++i) { final char extraChar = extraChars.charAt(i); if (name.indexOf(extraChar) != -1) { return true; } } return false; } protected String getQualifedNameDelimiter() { if (qualifiedNameDelimiter == null) { // Get the identifier quote string ... try { qualifiedNameDelimiter = this.getJdbcDatabase().getCapabilities().getCatalogSeparator(); if (qualifiedNameDelimiter != null && qualifiedNameDelimiter.trim().length() == 0) { qualifiedNameDelimiter = null; } } catch (JdbcException e) { JdbcPlugin.Util.log(e); // not expected, but log just in case } catch (SQLException e) { // ignore; } if (qualifiedNameDelimiter == null) { qualifiedNameDelimiter = DEFAULT_QUALIFIED_NAME_DELIMITER; } } return qualifiedNameDelimiter; } /** * Check whether the characters in the name are considered valid. The first character must be an alphabetic character. * * @param name the name to be checked; may not be null * @return boolean true if name is valid, false othewise */ public boolean isValidName( final String name ) { CoreArgCheck.isNotNull(name); boolean isValid = false; // Go through the string and ensure that each character is valid ... CharacterIterator charIter = new StringCharacterIterator(name); char c = charIter.first(); // The first character must be an alphabetic character ... if (c != CharacterIterator.DONE) { if (Character.isLetter(c)) { isValid = true; } } return isValid; } }