diff --git a/ide/db.sql.editor/src/org/netbeans/modules/db/sql/analyzer/SQLStatementAnalyzer.java b/ide/db.sql.editor/src/org/netbeans/modules/db/sql/analyzer/SQLStatementAnalyzer.java index 299e2a39140e..155cc61f8100 100644 --- a/ide/db.sql.editor/src/org/netbeans/modules/db/sql/analyzer/SQLStatementAnalyzer.java +++ b/ide/db.sql.editor/src/org/netbeans/modules/db/sql/analyzer/SQLStatementAnalyzer.java @@ -34,7 +34,6 @@ import org.netbeans.modules.db.sql.analyzer.SQLStatement.Context; import org.netbeans.modules.db.sql.editor.StringUtils; import org.netbeans.modules.db.sql.lexer.SQLTokenId; - /** * * @author Jiri Rechtacek, Jiri Skrivanek @@ -239,6 +238,10 @@ protected QualIdent parseIdentifier() { } protected String getUnquotedIdentifier() { + // quoter unavailable so returnas is + if (quoter == null) { + return seq.token().text().toString(); + } return quoter.unquote(seq.token().text().toString()); } diff --git a/ide/db.sql.editor/src/org/netbeans/modules/db/sql/editor/completion/SQLCompletionProvider.java b/ide/db.sql.editor/src/org/netbeans/modules/db/sql/editor/completion/SQLCompletionProvider.java index 8d3aa3d8a6a9..8659c6fe0bf9 100644 --- a/ide/db.sql.editor/src/org/netbeans/modules/db/sql/editor/completion/SQLCompletionProvider.java +++ b/ide/db.sql.editor/src/org/netbeans/modules/db/sql/editor/completion/SQLCompletionProvider.java @@ -22,7 +22,6 @@ import javax.swing.text.Document; import javax.swing.text.JTextComponent; import org.netbeans.api.db.explorer.DatabaseConnection; -import org.netbeans.api.editor.completion.Completion; import org.netbeans.api.lexer.Language; import org.netbeans.api.lexer.TokenHierarchy; import org.netbeans.api.lexer.TokenSequence; @@ -41,39 +40,61 @@ * @author Andrei Badea */ public class SQLCompletionProvider implements CompletionProvider { - + @Override public CompletionTask createTask(int queryType, JTextComponent component) { - if (queryType == CompletionProvider.COMPLETION_QUERY_TYPE || queryType == CompletionProvider.COMPLETION_ALL_QUERY_TYPE) { + if (queryType == CompletionProvider.COMPLETION_QUERY_TYPE || + queryType == CompletionProvider.COMPLETION_ALL_QUERY_TYPE) { + + /* to support DB related completion tasks (i.e. auto populating table + names or db columns for given schema) check for connection */ DatabaseConnection dbconn = findDBConn(component); - if (dbconn == null) { - // XXX perhaps should have an item in the completion instead? - Completion.get().hideAll(); - SQLExecutionBaseAction.notifyNoDatabaseConnection(); - return null; - } + return new AsyncCompletionTask(new SQLCompletionQuery(dbconn), component); } + + // not a completion query type so return nothing return null; } + /** + * getAutoQueryTypes is invoked to check whether a popup with suggestions + * should be shown without the user explicitly asking for it. + * + * If either #getAutoQueryTypes return a non-zero value or the user + * explicitly asks for completion, #createTask is invoked with the + * requested type. In case of SQL see + * org.netbeans.modules.db.sql.editor.completion.SQLCompletionQuery. + * + * @param component + * @param typedText + * @return + */ public int getAutoQueryTypes(JTextComponent component, String typedText) { + // XXX: Check if "enable/disable" autocomplete is setting. See NETBEANS-188 + // If "." has not been typed then acceptable to start checking for options. if (!".".equals(typedText)) { // NOI18N return 0; } + // check typed text if dot is present at the selected offset if (!isDotAtOffset(component, component.getSelectionStart() - 1)) { return 0; } + + // check if there is a DB connection DatabaseConnection dbconn = findDBConn(component); if (dbconn == null) { String message = NbBundle.getMessage(SQLCompletionProvider.class, "MSG_NoDatabaseConnection"); StatusDisplayer.getDefault().setStatusText(message); - return 0; + SQLExecutionBaseAction.notifyNoDatabaseConnection(); } - if (dbconn.getJDBCConnection() == null) { + + // check if DB connection is active + if (dbconn != null && dbconn.getJDBCConnection() == null) { String message = NbBundle.getMessage(SQLCompletionProvider.class, "MSG_NotConnected"); StatusDisplayer.getDefault().setStatusText(message); - return 0; - } + SQLExecutionBaseAction.notifyNoDatabaseConnection(); + // XXX: Maybe add content specific "fixs" + } return COMPLETION_QUERY_TYPE; } @@ -101,24 +122,35 @@ private static Lookup findContext(JTextComponent component) { return null; } + /** + * For given component object's doc model, determine if given offset + * has applicable dot delimiter token + * @param component + * @param offset + * @return + */ private static boolean isDotAtOffset(JTextComponent component, final int offset) { final Document doc = component.getDocument(); final boolean[] result = { false }; - doc.render(new Runnable() { - public void run() { - TokenSequence seq = getSQLTokenSequence(doc); - if (seq == null) { - return; - } - seq.move(offset); - if (!seq.moveNext() && !seq.movePrevious()) { - return; - } - if (seq.offset() != offset) { - return; - } - result[0] = (seq.token().id() == SQLTokenId.DOT); + // trigger internal model rendering based on SQL tokens + doc.render(() -> { + TokenSequence seq = getSQLTokenSequence(doc); + // no SQL tokens found; done checking + if (seq == null) { + return; + } + // in the sequence move to provided offset location + seq.move(offset); + // no next or previous so done looking for tokens; done checking + if (!seq.moveNext() && !seq.movePrevious()) { + return; + } + // if offset not consistent with sequence offset done checking + if (seq.offset() != offset) { + return; } + // confirm token ID is applicable SQL "dot" token + result[0] = (seq.token().id() == SQLTokenId.DOT); }); return result[0]; } diff --git a/ide/db.sql.editor/src/org/netbeans/modules/db/sql/editor/completion/SQLCompletionQuery.java b/ide/db.sql.editor/src/org/netbeans/modules/db/sql/editor/completion/SQLCompletionQuery.java index 57fd2a334696..51a47122dde4 100644 --- a/ide/db.sql.editor/src/org/netbeans/modules/db/sql/editor/completion/SQLCompletionQuery.java +++ b/ide/db.sql.editor/src/org/netbeans/modules/db/sql/editor/completion/SQLCompletionQuery.java @@ -57,6 +57,7 @@ import org.netbeans.modules.db.sql.analyzer.SQLStatementKind; import org.netbeans.modules.db.sql.analyzer.UpdateStatement; import org.netbeans.modules.db.sql.editor.api.completion.SQLCompletionResultSet; +import org.netbeans.modules.db.sql.editor.ui.actions.SQLExecutionBaseAction; import org.netbeans.modules.db.sql.lexer.SQLTokenId; import org.netbeans.spi.editor.completion.CompletionResultSet; import org.netbeans.spi.editor.completion.support.AsyncCompletionQuery; @@ -120,23 +121,42 @@ public void query(SQLCompletionResultSet resultSet, SQLCompletionEnv newEnv) { private void doQuery(final SQLCompletionEnv newEnv) { try { - DBConnMetadataModelManager.get(dbconn).runReadAction(new Action() { - @Override - public void run(Metadata metadata) { - Connection conn = dbconn.getJDBCConnection(); - if (conn == null) { - return; - } - Quoter quoter = null; - try { - DatabaseMetaData dmd = conn.getMetaData(); - quoter = SQLIdentifiers.createQuoter(dmd); - } catch (SQLException e) { - throw new MetadataException(e); + // DB Connection available + if (dbconn != null) { + // DB connection present so + DBConnMetadataModelManager.get(dbconn).runReadAction(new Action() { + @Override + public void run(Metadata metadata) { + Connection conn = null; + if (dbconn != null) { + conn = dbconn.getJDBCConnection(); + } + Quoter quoter = null; + try { + /* if connection available allow for bb meta data + and quoter to help in auto completion */ + if (conn != null) { + // get Database meta data + DatabaseMetaData dmd = conn.getMetaData(); + quoter = SQLIdentifiers.createQuoter(dmd); + } + } catch (SQLException e) { + throw new MetadataException(e); + } + /* if quoter available then allow for query for + auto completion to occur, else avoid this sort of + activities when quoter/connection not available*/ + if (quoter != null) { + doQuery(newEnv, metadata, quoter); + } else { + return; + } } - doQuery(newEnv, metadata, quoter); - } - }); + }); + } else { + // No DB Connection established presently + doQuery(newEnv, metadata, quoter); + } } catch (MetadataModelException e) { reportError(e); } @@ -149,33 +169,49 @@ SQLCompletionItems doQuery(SQLCompletionEnv env, Metadata metadata, Quoter quote this.quoter = quoter; anchorOffset = -1; substitutionOffset = 0; + + // address empty env so add basic keywords items = new SQLCompletionItems(quoter, env.getSubstitutionHandler()); + if (env.getTokenSequence().isEmpty()) { completeKeyword("SELECT", "INSERT", "DELETE", "DROP", "UPDATE", "CREATE"); //NOI18N return items; } - statement = SQLStatementAnalyzer.analyze(env.getTokenSequence(), quoter); + + // not empty so address possible statement cases + statement = SQLStatementAnalyzer.analyze(env.getTokenSequence(), quoter); + + // unable to find relevant case so include basic keywords items if (statement == null) { completeKeyword("SELECT", "INSERT", "DELETE", "DROP", "UPDATE", "CREATE"); //NOI18N return items; } + + // found a create case so update to include create case items if (statement.getKind() == SQLStatementKind.CREATE && ((CreateStatement) statement).hasBody()) { completeCreateBody(); return items; } + + // checking context of offset to see if applicable; if not then set to basics context = statement.getContextAtOffset(env.getCaretOffset()); if (context == null) { completeKeyword("SELECT", "INSERT", "DELETE", "DROP", "UPDATE", "CREATE"); //NOI18N return items; } + + // from context look for identifiers; if none then return current context ident = findIdentifier(); if (ident == null) { completeKeyword(context); return items; } + + // from ident anchorif identifier identified then determine kin anchorOffset = ident.anchorOffset; substitutionOffset = ident.substitutionOffset; SQLStatementKind kind = statement.getKind(); + switch (kind) { case SELECT: completeSelect(); @@ -474,40 +510,46 @@ private void completeColumnWithDefinedTuple(Identifier ident) { } } - /** Adds columns, tuples, schemas and catalogs according to given identifier. */ + /** + * Adds columns, tuples, schema and catalogs according to given identifier. + */ private void completeColumnSimpleIdent(String typedPrefix, boolean quoted) { if (tablesClause != null && !(tablesClause.getUnaliasedTableNames().isEmpty() && tablesClause.getAliasedTableNames().isEmpty())) { completeSimpleIdentBasedOnFromClause(typedPrefix, quoted); } else { - Schema defaultSchema = metadata.getDefaultSchema(); - if (defaultSchema != null) { - // All columns in default schema, but only if a prefix has been typed, otherwise there - // would be too many columns. - if (typedPrefix != null) { - for (Table table : defaultSchema.getTables()) { - items.addColumns(table, typedPrefix, quoted, substitutionOffset); + // have database metadata to populate + if (metadata != null) { + Schema defaultSchema = metadata.getDefaultSchema(); + if (defaultSchema != null) { + // All columns in default schema, but only if a prefix has been typed, otherwise there + // would be too many columns. + if (typedPrefix != null) { + for (Table table : defaultSchema.getTables()) { + items.addColumns(table, typedPrefix, quoted, substitutionOffset); + } + if (includeViews) { + for (View view : defaultSchema.getViews()) { + items.addColumns(view, typedPrefix, quoted, substitutionOffset); + } + } } + // All tuples in default schema. + items.addTables(defaultSchema, null, typedPrefix, quoted, substitutionOffset); if (includeViews) { - for (View view : defaultSchema.getViews()) { - items.addColumns(view, typedPrefix, quoted, substitutionOffset); - } + items.addViews(defaultSchema, null, typedPrefix, quoted, substitutionOffset); } } - // All tuples in default schema. - items.addTables(defaultSchema, null, typedPrefix, quoted, substitutionOffset); - if (includeViews) { - items.addViews(defaultSchema, null, typedPrefix, quoted, substitutionOffset); - } + // All schemas. + Catalog defaultCatalog = metadata.getDefaultCatalog(); + items.addSchemas(defaultCatalog, null, typedPrefix, quoted, substitutionOffset); + // All catalogs. + items.addCatalogs(metadata, null, typedPrefix, quoted, substitutionOffset); } - // All schemas. - Catalog defaultCatalog = metadata.getDefaultCatalog(); - items.addSchemas(defaultCatalog, null, typedPrefix, quoted, substitutionOffset); - // All catalogs. - items.addCatalogs(metadata, null, typedPrefix, quoted, substitutionOffset); } } private void completeColumnWithTupleIfSimpleIdent(String typedPrefix, boolean quoted) { + if (metadata != null) { Schema defaultSchema = metadata.getDefaultSchema(); if (defaultSchema != null) { // All columns in default schema, but only if a prefix has been typed, otherwise there @@ -534,6 +576,7 @@ private void completeColumnWithTupleIfSimpleIdent(String typedPrefix, boolean qu items.addSchemas(defaultCatalog, null, typedPrefix, quoted, substitutionOffset); // All catalogs. items.addCatalogs(metadata, null, typedPrefix, quoted, substitutionOffset); + } } private void completeColumnWithTupleIfQualIdent(QualIdent fullyTypedIdent, String lastPrefix, boolean quoted) { @@ -587,19 +630,22 @@ private void completeColumnQualIdent(QualIdent fullyTypedIdent, String lastPrefi /** Adds all tuples from default schema, all schemas from defaultcatalog * and all catalogs. */ private void completeTupleSimpleIdent(String typedPrefix, boolean quoted) { - Schema defaultSchema = metadata.getDefaultSchema(); - if (defaultSchema != null) { - // All tuples in default schema. - items.addTables(defaultSchema, null, typedPrefix, quoted, substitutionOffset); - if (includeViews) { - items.addViews(defaultSchema, null, typedPrefix, quoted, substitutionOffset); + // connection metadata available so add to available items + if (metadata != null ){ + Schema defaultSchema = metadata.getDefaultSchema(); + if (defaultSchema != null) { + // All tuples in default schema. + items.addTables(defaultSchema, null, typedPrefix, quoted, substitutionOffset); + if (includeViews) { + items.addViews(defaultSchema, null, typedPrefix, quoted, substitutionOffset); + } } + // All schemas. + Catalog defaultCatalog = metadata.getDefaultCatalog(); + items.addSchemas(defaultCatalog, null, typedPrefix, quoted, substitutionOffset); + // All catalogs. + items.addCatalogs(metadata, null, typedPrefix, quoted, substitutionOffset); } - // All schemas. - Catalog defaultCatalog = metadata.getDefaultCatalog(); - items.addSchemas(defaultCatalog, null, typedPrefix, quoted, substitutionOffset); - // All catalogs. - items.addCatalogs(metadata, null, typedPrefix, quoted, substitutionOffset); } /** Adds all tuples in schema get from fully qualified identifier or all @@ -643,6 +689,7 @@ private void completeSimpleIdentBasedOnFromClause(String typedPrefix, boolean qu items.addColumns(tuple, typedPrefix, quoted, substitutionOffset); } // Tuples from default schema, restricted to non-aliased tuple names in the FROM clause. + if (metadata != null ) { Schema defaultSchema = metadata.getDefaultSchema(); if (defaultSchema != null) { Set simpleTupleNames = new TreeSet(); @@ -674,6 +721,7 @@ private void completeSimpleIdentBasedOnFromClause(String typedPrefix, boolean qu Catalog defaultCatalog = metadata.getDefaultCatalog(); items.addSchemas(defaultCatalog, schemaNames, typedPrefix, quoted, substitutionOffset); items.addCatalogs(metadata, catalogNames, typedPrefix, quoted, substitutionOffset); + } } private void completeQualIdentBasedOnFromClause(QualIdent fullyTypedIdent, String lastPrefix, boolean quoted) { @@ -744,7 +792,7 @@ private void completeCatalog(Catalog catalog, String prefix, boolean quoted) { } private Catalog resolveCatalog(QualIdent catalogName) { - if (catalogName.isSimple()) { + if (metadata != null && catalogName.isSimple()) { return metadata.getCatalog(catalogName.getSimpleName()); } return null; @@ -752,23 +800,25 @@ private Catalog resolveCatalog(QualIdent catalogName) { private Schema resolveSchema(QualIdent schemaName) { Schema schema = null; - switch (schemaName.size()) { - case 1: - Catalog catalog = metadata.getDefaultCatalog(); - schema = catalog.getSchema(schemaName.getSimpleName()); - break; - case 2: - catalog = metadata.getCatalog(schemaName.getFirstQualifier()); - if (catalog != null) { + if (metadata != null) { + switch (schemaName.size()) { + case 1: + Catalog catalog = metadata.getDefaultCatalog(); schema = catalog.getSchema(schemaName.getSimpleName()); - } - break; + break; + case 2: + catalog = metadata.getCatalog(schemaName.getFirstQualifier()); + if (catalog != null) { + schema = catalog.getSchema(schemaName.getSimpleName()); + } + break; + } } return schema; } private Tuple resolveTuple(QualIdent tupleName) { - if (tupleName == null) { + if (tupleName == null || metadata == null) { return null; } Tuple tuple = null; @@ -918,7 +968,7 @@ private Identifier findIdentifier() { String part; int offset = caretOffset - seq.offset(); String tokenText = seq.token().text().toString(); - if (offset > 0 && offset < seq.token().length()) { + if (offset > 0 && offset < seq.token().length() && quoter != null) { String quoteString = quoter.getQuoteString(); if (tokenText.startsWith(quoteString) && tokenText.endsWith(quoteString) && offset == tokenText.length() - 1) { // identifier inside closed quotes and cursor before ending quote ("foo|") @@ -978,6 +1028,10 @@ private Identifier findIdentifier() { } /** + * Used to create an Identifier based on current parts of the expression so far, + * if the details are "Complete" and the offset of given prefix involved. + * + * @param parts the list of potentail * @param lastPrefixOffset the offset of the last prefix in the identifier, or * if no such prefix, the caret offset. * @return @@ -993,30 +1047,32 @@ private Identifier createIdentifier(List parts, boolean incomplete, int } // Fine, nothing was typed. } else { - if (!incomplete) { - lastPrefix = parts.remove(parts.size() - 1); - String quoteString = quoter.getQuoteString(); - if (quoteString.length() > 0 && lastPrefix.startsWith(quoteString)) { - if (lastPrefix.endsWith(quoteString) && lastPrefix.length() > quoteString.length()) { - // User typed '"foo"."bar"|', can't complete that. + if (quoter != null ) { + if (!incomplete) { + lastPrefix = parts.remove(parts.size() - 1); + String quoteString = quoter.getQuoteString(); + if (quoteString.length() > 0 && lastPrefix.startsWith(quoteString)) { + if (lastPrefix.endsWith(quoteString) && lastPrefix.length() > quoteString.length()) { + // User typed '"foo"."bar"|', can't complete that. + return null; + } + int lastPrefixLength = lastPrefix.length(); + lastPrefix = quoter.unquote(lastPrefix); + lastPrefixOffset = lastPrefixOffset + (lastPrefixLength - lastPrefix.length()); + quoted = true; + } else if (quoteString.length() > 0 && lastPrefix.endsWith(quoteString)) { + // User typed '"foo".bar"|', can't complete. + return null; + } + } + for (int i = 0; i < parts.size(); i++) { + String unquoted = quoter.unquote(parts.get(i)); + if (unquoted.length() == 0) { + // User typed something like '"foo".""."bar|'. return null; } - int lastPrefixLength = lastPrefix.length(); - lastPrefix = quoter.unquote(lastPrefix); - lastPrefixOffset = lastPrefixOffset + (lastPrefixLength - lastPrefix.length()); - quoted = true; - } else if (quoteString.length() > 0 && lastPrefix.endsWith(quoteString)) { - // User typed '"foo".bar"|', can't complete. - return null; - } - } - for (int i = 0; i < parts.size(); i++) { - String unquoted = quoter.unquote(parts.get(i)); - if (unquoted.length() == 0) { - // User typed something like '"foo".""."bar|'. - return null; + parts.set(i, unquoted); } - parts.set(i, unquoted); } } return new Identifier(new QualIdent(parts), lastPrefix, quoted, lastPrefixOffset, substOffset);