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..ab9214c13966 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,7 @@ 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; - +import org.openide.util.Parameters; /** * * @author Jiri Rechtacek, Jiri Skrivanek @@ -49,6 +49,8 @@ public class SQLStatementAnalyzer { protected final List subqueries = new ArrayList(); protected SQLStatementAnalyzer(TokenSequence seq, Quoter quoter) { + Parameters.notNull("seq", seq); + Parameters.notNull("quoter", quoter); this.seq = seq; this.quoter = quoter; } 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..9753981b9ebe 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,12 +22,10 @@ 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; import org.netbeans.modules.db.api.sql.execute.SQLExecution; -import org.netbeans.modules.db.sql.editor.ui.actions.SQLExecutionBaseAction; import org.netbeans.modules.db.sql.lexer.SQLTokenId; import org.netbeans.spi.editor.completion.CompletionProvider; import org.netbeans.spi.editor.completion.CompletionTask; @@ -41,39 +39,60 @@ * @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 + */ + @Override 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; } - if (dbconn.getJDBCConnection() == null) { + + // check and notify if DB connection is inactive + if (dbconn != null && dbconn.getJDBCConnection() == null) { String message = NbBundle.getMessage(SQLCompletionProvider.class, "MSG_NotConnected"); StatusDisplayer.getDefault().setStatusText(message); - return 0; } + return COMPLETION_QUERY_TYPE; } @@ -101,24 +120,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..3bd5979f4524 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 @@ -120,23 +120,38 @@ 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*/ + doQuery(newEnv, metadata, quoter); } - doQuery(newEnv, metadata, quoter); - } - }); + }); + } else { + // No DB Connection established presently + doQuery(newEnv, metadata, quoter == null ? SQLIdentifiers.createQuoter(null) : quoter); + } } catch (MetadataModelException e) { reportError(e); } @@ -149,33 +164,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(); @@ -218,7 +249,7 @@ private void completeCreate() { completeSelect(); } } - + private void completeSelect() { SelectStatement selectStatement = (SelectStatement) statement; tablesClause = selectStatement.getTablesInEffect(env.getCaretOffset()); @@ -474,29 +505,65 @@ 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 { + // 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) { + 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); + } + } + } + + 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 // would be too many columns. if (typedPrefix != null) { for (Table table : defaultSchema.getTables()) { - items.addColumns(table, typedPrefix, quoted, substitutionOffset); + items.addColumnsWithTupleName(table, null, typedPrefix, quoted, substitutionOffset - 1); } if (includeViews) { for (View view : defaultSchema.getViews()) { - items.addColumns(view, typedPrefix, quoted, substitutionOffset); + items.addColumnsWithTupleName(view, null, typedPrefix, quoted, substitutionOffset - 1); } } - } - // All tuples in default schema. - items.addTables(defaultSchema, null, typedPrefix, quoted, substitutionOffset); - if (includeViews) { - items.addViews(defaultSchema, null, typedPrefix, quoted, substitutionOffset); + } else { + // All tuples in default schema. + items.addTablesAtInsertInto(defaultSchema, null, null, typedPrefix, quoted, substitutionOffset - 1); + if (includeViews) { + items.addViewsAtInsertInto(defaultSchema, null, null, typedPrefix, quoted, substitutionOffset - 1); + } } } // All schemas. @@ -507,35 +574,6 @@ private void completeColumnSimpleIdent(String typedPrefix, boolean quoted) { } } - private void completeColumnWithTupleIfSimpleIdent(String typedPrefix, boolean quoted) { - 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.addColumnsWithTupleName(table, null, typedPrefix, quoted, substitutionOffset - 1); - } - if (includeViews) { - for (View view : defaultSchema.getViews()) { - items.addColumnsWithTupleName(view, null, typedPrefix, quoted, substitutionOffset - 1); - } - } - } else { - // All tuples in default schema. - items.addTablesAtInsertInto (defaultSchema, null, null, typedPrefix, quoted, substitutionOffset - 1); - if (includeViews) { - items.addViewsAtInsertInto(defaultSchema, null, null, typedPrefix, quoted, substitutionOffset - 1); - } - } - } - // All schemas. - Catalog defaultCatalog = metadata.getDefaultCatalog(); - 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) { // Assume fullyTypedIdent is a tuple. Tuple tuple = resolveTuple(fullyTypedIdent); @@ -587,19 +625,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 @@ -624,8 +665,8 @@ private void completeSimpleIdentBasedOnFromClause(String typedPrefix, boolean qu assert tablesClause != null; Set tupleNames = tablesClause.getUnaliasedTableNames(); Set tuples = resolveTuples(tupleNames); - Set allTupleNames = new TreeSet(tupleNames); - Set allTuples = new LinkedHashSet(tuples); + Set allTupleNames = new TreeSet<>(tupleNames); + Set allTuples = new LinkedHashSet<>(tuples); Map aliases = tablesClause.getAliasedTableNames(); for (Entry entry : aliases.entrySet()) { QualIdent tupleName = entry.getValue(); @@ -636,44 +677,46 @@ private void completeSimpleIdentBasedOnFromClause(String typedPrefix, boolean qu } } // Aliases. - Map sortedAliases = new TreeMap(aliases); + Map sortedAliases = new TreeMap<>(aliases); items.addAliases(sortedAliases, typedPrefix, quoted, substitutionOffset); // Columns from aliased and non-aliased tuples in the FROM clause. for (Tuple tuple : allTuples) { items.addColumns(tuple, typedPrefix, quoted, substitutionOffset); } // Tuples from default schema, restricted to non-aliased tuple names in the FROM clause. - Schema defaultSchema = metadata.getDefaultSchema(); - if (defaultSchema != null) { - Set simpleTupleNames = new TreeSet(); - for (Tuple tuple : tuples) { - if (tuple.getParent().isDefault()) { - simpleTupleNames.add(tuple.getName()); + if (metadata != null) { + Schema defaultSchema = metadata.getDefaultSchema(); + if (defaultSchema != null) { + Set simpleTupleNames = new TreeSet<>(); + for (Tuple tuple : tuples) { + if (tuple.getParent().isDefault()) { + simpleTupleNames.add(tuple.getName()); + } + } + items.addTables(defaultSchema, simpleTupleNames, typedPrefix, quoted, substitutionOffset); + if (includeViews) { + items.addViews(defaultSchema, simpleTupleNames, typedPrefix, quoted, substitutionOffset); } } - items.addTables(defaultSchema, simpleTupleNames, typedPrefix, quoted, substitutionOffset); - if (includeViews) { - items.addViews(defaultSchema, simpleTupleNames, typedPrefix, quoted, substitutionOffset); - } - } - // Schemas from default catalog other than the default schema, based on non-aliased tuple names in the FROM clause. - // Catalogs based on non-aliased tuples names in the FROM clause. - Set schemaNames = new TreeSet(); - Set catalogNames = new TreeSet(); - for (Tuple tuple : tuples) { - Schema schema = tuple.getParent(); - Catalog catalog = schema.getParent(); - if (!schema.isDefault() && !schema.isSynthetic() && catalog.isDefault()) { - schemaNames.add(schema.getName()); - } - if (!catalog.isDefault()) { - catalogNames.add(catalog.getName()); - } + // Schemas from default catalog other than the default schema, based on non-aliased tuple names in the FROM clause. + // Catalogs based on non-aliased tuples names in the FROM clause. + Set schemaNames = new TreeSet<>(); + Set catalogNames = new TreeSet<>(); + for (Tuple tuple : tuples) { + Schema schema = tuple.getParent(); + Catalog catalog = schema.getParent(); + if (!schema.isDefault() && !schema.isSynthetic() && catalog.isDefault()) { + schemaNames.add(schema.getName()); + } + if (!catalog.isDefault()) { + catalogNames.add(catalog.getName()); + } + } + Catalog defaultCatalog = metadata.getDefaultCatalog(); + items.addSchemas(defaultCatalog, schemaNames, typedPrefix, quoted, substitutionOffset); + items.addCatalogs(metadata, catalogNames, typedPrefix, quoted, substitutionOffset); } - 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) { @@ -698,7 +741,7 @@ private void completeQualIdentBasedOnFromClause(QualIdent fullyTypedIdent, Strin // Now assume fullyTypedIdent is the name of a schema in the default catalog. Schema schema = resolveSchema(fullyTypedIdent); if (schema != null) { - Set tupleNames = new TreeSet(); + Set tupleNames = new TreeSet<>(); for (Tuple tuple : tuples) { if (tuple.getParent().equals(schema)) { tupleNames.add(tuple.getName()); @@ -712,8 +755,8 @@ private void completeQualIdentBasedOnFromClause(QualIdent fullyTypedIdent, Strin // Now assume fullyTypedIdent is the name of a catalog. Catalog catalog = resolveCatalog(fullyTypedIdent); if (catalog != null) { - Set syntheticSchemaTupleNames = new TreeSet(); - Set schemaNames = new TreeSet(); + Set syntheticSchemaTupleNames = new TreeSet<>(); + Set schemaNames = new TreeSet<>(); for (Tuple tuple : tuples) { schema = tuple.getParent(); if (schema.getParent().equals(catalog)) { @@ -744,7 +787,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,26 +795,28 @@ 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; + Tuple tuple; switch (tupleName.size()) { case 1: Schema schema = metadata.getDefaultSchema(); @@ -824,7 +869,7 @@ private Tuple getTuple(Schema schema, QualIdent tupleName) { } private Set resolveTuples(Set tupleNames) { - Set result = new LinkedHashSet(tupleNames.size()); + Set result = new LinkedHashSet<>(tupleNames.size()); for (QualIdent tupleName : tupleNames) { Tuple tuple = resolveTuple(tupleName); if (tuple != null) { @@ -840,7 +885,7 @@ private Set resolveTuples(Set tupleNames) { private Symbol findPrefix() { TokenSequence seq = env.getTokenSequence(); int caretOffset = env.getCaretOffset(); - String prefix = null; + String prefix; if (seq.move(caretOffset) > 0) { // Not on token boundary. if (!seq.moveNext() && !seq.movePrevious()) { @@ -873,7 +918,7 @@ private Symbol findPrefix() { private Identifier findIdentifier() { TokenSequence seq = env.getTokenSequence(); int caretOffset = env.getCaretOffset(); - final List parts = new ArrayList(); + final List parts = new ArrayList<>(); if (seq.move(caretOffset) > 0) { // Not on token boundary. if (!seq.moveNext() && !seq.movePrevious()) { @@ -978,6 +1023,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 diff --git a/ide/db/apichanges.xml b/ide/db/apichanges.xml index 90fcf69ee597..a6b649074a10 100644 --- a/ide/db/apichanges.xml +++ b/ide/db/apichanges.xml @@ -83,6 +83,23 @@ is the proper place. + + + Return a fallback quoter if no DatabaseMetaData is available. + + + + + + SQLIdentifiers now returns a fallback + Quoter if there is no DatabaseMetaData. + The fallback quoter supports unquoting most common identifier + quoting formats, uses SQL-99 quotes for quoting and quotes all + identifiers, that don't start with an ascii character or + contain ascii non-characters or non-numbers. + + + Concept of preferred connection introduced diff --git a/ide/db/nbproject/project.properties b/ide/db/nbproject/project.properties index f3dd9544cc99..4f62a7a94717 100644 --- a/ide/db/nbproject/project.properties +++ b/ide/db/nbproject/project.properties @@ -20,7 +20,7 @@ javac.source=1.8 javadoc.arch=${basedir}/arch.xml javadoc.apichanges=${basedir}/apichanges.xml -spec.version.base=1.83.0 +spec.version.base=1.84.0 extra.module.files=modules/ext/ddl.jar diff --git a/ide/db/src/org/netbeans/api/db/sql/support/SQLIdentifiers.java b/ide/db/src/org/netbeans/api/db/sql/support/SQLIdentifiers.java index fa50189d757f..185591afe017 100644 --- a/ide/db/src/org/netbeans/api/db/sql/support/SQLIdentifiers.java +++ b/ide/db/src/org/netbeans/api/db/sql/support/SQLIdentifiers.java @@ -24,6 +24,7 @@ import java.sql.SQLException; import java.util.logging.Level; import java.util.logging.Logger; +import java.util.regex.Pattern; import org.openide.util.Lookup; import org.openide.util.Parameters; @@ -48,7 +49,11 @@ private SQLIdentifiers() { * @return a {@code Quoter} instance. */ public static Quoter createQuoter(DatabaseMetaData dbmd) { - return new DatabaseMetaDataQuoter(dbmd); + if(dbmd == null) { + return new FallbackQuoter(); + } else { + return new DatabaseMetaDataQuoter(dbmd); + } } @@ -365,4 +370,66 @@ private static int getCaseRule(DatabaseMetaData dbmd) { } } + private static class FallbackQuoter extends Quoter { + + private static final Pattern ASCII_IDENTIFIER = Pattern.compile("[a-zA-z][a-zA-Z0-9_]+"); + + public FallbackQuoter() { + super("\""); + } + + @Override + boolean alreadyQuoted(String identifier) { + Parameters.notNull("identifier", identifier); + return getEndQuoteString(identifier) != null; + } + + @Override + public String quoteIfNeeded(String identifier) { + Parameters.notNull("identifier", identifier); + if (!alreadyQuoted(identifier) && !ASCII_IDENTIFIER.matcher(identifier).matches()) { + return doQuote(identifier); + } else { + return identifier; + } + } + + @Override + public String quoteAlways(String identifier) { + Parameters.notNull("identifier", identifier); + if (!alreadyQuoted(identifier)) { + return doQuote(identifier); + } else { + return identifier; + } + } + + private String getEndQuoteString(String identifier) { + if (identifier.startsWith("\"") && identifier.endsWith("\"")) { + return "\""; + } else if (identifier.startsWith("`") && identifier.endsWith("`")) { + return "`"; + } else if (identifier.startsWith("[") && identifier.endsWith("]")) { + return "]"; + } + return null; + } + + @Override + public String unquote(String identifier) { + Parameters.notNull("identifier", identifier); + String workidentifier = identifier.trim(); + String endQuoteString = getEndQuoteString(identifier); + if(endQuoteString == null) { + return workidentifier; + } + + String result = workidentifier; + // Extract the contents of the quoted string + result = result.substring(1, result.length() - 1); + // remove potentially present quotes + result = result.replace(endQuoteString + endQuoteString, endQuoteString); + return result; + } + } } diff --git a/ide/db/test/unit/src/org/netbeans/api/db/sql/support/SQLIdentifiersTest.java b/ide/db/test/unit/src/org/netbeans/api/db/sql/support/SQLIdentifiersTest.java new file mode 100644 index 000000000000..7ce40ed3ed6f --- /dev/null +++ b/ide/db/test/unit/src/org/netbeans/api/db/sql/support/SQLIdentifiersTest.java @@ -0,0 +1,48 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.netbeans.api.db.sql.support; + +import org.junit.Test; +import org.netbeans.api.db.sql.support.SQLIdentifiers.Quoter; + +import static org.junit.Assert.*; + +public class SQLIdentifiersTest { + @Test + public void testQuoterFromNull() { + // Without DatabaseMetaData a fallback quoter is returned, that + // unquotes all popular identifier quotes (SQL-99, mssql and mysql) and + // quotes with SQL-99 quotes (") + Quoter quoter = SQLIdentifiers.createQuoter(null); + assertNotNull(quoter); + + assertEquals("hello\"", quoter.unquote("hello\"")); + assertEquals("\"hello", quoter.unquote("\"hello")); + assertEquals("hello", quoter.unquote("\"hello\"")); + assertEquals("hello`", quoter.unquote("hello`")); + assertEquals("`hello", quoter.unquote("`hello")); + assertEquals("hello", quoter.unquote("`hello`")); + assertEquals("hello]", quoter.unquote("hello]")); + assertEquals("[hello", quoter.unquote("[hello")); + assertEquals("hello", quoter.unquote("[hello]")); + assertEquals("hello", quoter.quoteIfNeeded("hello")); + assertEquals("\"hello world\"", quoter.quoteIfNeeded("hello world")); + assertEquals("\"hello\"", quoter.quoteAlways("hello")); + } +}