Index: apichanges.xml
===================================================================
RCS file: /cvs/db/apichanges.xml,v
retrieving revision 1.6
diff -u -u -r1.6 apichanges.xml
--- apichanges.xml 18 Jun 2007 17:40:42 -0000 1.6
+++ apichanges.xml 11 Sep 2007 21:12:08 -0000
@@ -84,6 +84,21 @@
+ Added utility class for working with SQL identifiers
+
+
+
+
+
+ This change adds a SQLIdentifier
class which
+ provides utility methods for working with SQL identifiers, such
+ as quoting the identifier if needed.
+
+
+
+
+
+
Added support for dragging and dropping nodes from the Database Explorer
Index: arch.xml
===================================================================
RCS file: /cvs/db/arch.xml,v
retrieving revision 1.15
diff -u -u -r1.15 arch.xml
--- arch.xml 11 Jul 2007 20:48:15 -0000 1.15
+++ arch.xml 11 Sep 2007 21:12:08 -0000
@@ -296,6 +296,14 @@
encapsulating those database objects during a drag and drop transfer.
+
+
+ A component might need support for working with SQL identifiers. In particular,
+ it's important to know when to quote a SQL identifier. The
+
+ SQLIdentifier class is provided for this.
+
+
Index: src/org/netbeans/api/db/explorer/support/SQLIdentifiers.java
===================================================================
RCS file: src/org/netbeans/api/db/explorer/support/SQLIdentifiers.java
diff -N src/org/netbeans/api/db/explorer/support/SQLIdentifiers.java
--- /dev/null 1 Jan 1970 00:00:00 -0000
+++ src/org/netbeans/api/db/explorer/support/SQLIdentifiers.java 11 Sep 2007 21:12:08 -0000
@@ -0,0 +1,239 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development
+ * and Distribution License (the License). You may not use this file except in
+ * compliance with the License.
+ *
+ * You can obtain a copy of the License at http://www.netbeans.org/cddl.html
+ * or http://www.netbeans.org/cddl.txt.
+ *
+ * When distributing Covered Code, include this CDDL Header Notice in each file
+ * and include the License file at http://www.netbeans.org/cddl.txt.
+ * If applicable, add the following below the CDDL Header, with the fields
+ * enclosed by brackets [] replaced by your own identifying information:
+ * "Portions Copyrighted [year] [name of copyright owner]"
+ *
+ * Portions Copyrighted 2007 Sun Microsystems, Inc.
+ */
+package org.netbeans.api.db.explorer.support;
+
+import java.sql.DatabaseMetaData;
+import java.sql.SQLException;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import org.openide.util.Parameters;
+
+/**
+ * This class provides utility methods for working with SQL identifiers
+ */
+public final class SQLIdentifiers {
+ private static final Logger LOGGER =
+ Logger.getLogger(SQLIdentifiers.class.getName());
+
+ /**
+ * Construct an instance of SQLIdentifier.
+ *
+ * @param dbmd The DatabaseMetaData to use when working with identifiers.
+ * The metadata object is used to determine when an identifier needs
+ * to be quoted and what the quote string should be.
+ */
+ public static Quoter createQuoter(DatabaseMetaData dbmd) {
+ return new Quoter(dbmd);
+ }
+
+
+ /**
+ * This is a utility class that is used to quote identifiers.
+ *
+ * This class is immutable and thus thread-safe
+ */
+ public static class Quoter {
+ private static final Logger LOGGER =
+ Logger.getLogger(Quoter.class.getName());
+
+ // Rules for what happens to the casing of a character in an identifier
+ // when it is not quoted
+ private static final int LC_RULE = 0; // everything goes to lower case
+ private static final int UC_RULE = 1; // everything goes to upper case
+ private static final int MC_RULE = 2; // mixed case remains mixed case
+
+ private final String extraNameChars;
+ private final String quoteString;
+ private final int caseRule;
+
+ private Quoter(DatabaseMetaData dbmd) {
+ extraNameChars = getExtraNameChars(dbmd);
+ quoteString = getQuoteString(dbmd);
+ caseRule = getCaseRule(dbmd);
+ }
+
+ /**
+ * Quote an existing identifier to be used in a SQL command,
+ * if needed.
+ *
+ * Anyone generating SQL that will be
+ * visible and/or editable by the user should use this method.
+ * This helps to avoid unecessary quoting, which affects the
+ * readability and clarity of the resulting SQL.
+ *
+ * An identifier needs to be quoted if one of the following is true:
+ *
+ * - any character in the
+ * string is not within the set of characters that do
+ * not need to be quoted in a SQL identifier.
+ *
+ *
- any character in the string is not of the
+ * expected casing (e.g. lower case when the database upper-cases
+ * all non-quoted identifiers).
+ *
+ *
+ * @param identifier a SQL identifier. Can not be null.
+ *
+ * @return the identifier, quoted if needed
+ */
+ public final String quoteIfNeeded(String identifier) {
+ Parameters.notNull("identifier", identifier);
+
+ if ( needToQuote(identifier) ) {
+ return quoteString + identifier + quoteString;
+ }
+
+ return identifier;
+ }
+
+ /**
+ * Determine if we need to quote this identifier
+ */
+ private boolean needToQuote(String identifier) {
+ if ( identifier == null ) {
+ throw new NullPointerException("identifier can not be null");
+ }
+
+ // No need to quote if it's already quoted
+ if ( identifier.startsWith(quoteString) &&
+ identifier.endsWith(quoteString)) {
+ return false;
+ }
+
+ int length = identifier.length();
+ for ( int i = 0 ; i < length ; i++ ) {
+ if ( charNeedsQuoting(identifier.charAt(i))) {
+ return true;
+ }
+ }
+
+ // Next, check to see if any characters are in the wrong casing
+ // (for example, if the db upper cases all non-quoted identifiers,
+ // and we have a lower-case character, then we need to quote
+ if ( caseRule == UC_RULE && containsLowerCase(identifier)) {
+ return true;
+ } else if ( caseRule == LC_RULE && containsUpperCase(identifier)) {
+ return true;
+ }
+
+ return false;
+ }
+
+ private boolean charNeedsQuoting(char ch) {
+ // Standard set of characters for SQL identifiers
+ if ( isUpperCase(ch) || isLowerCase(ch) ||
+ isNumber(ch) || ch == '_') {
+ return false;
+ }
+
+ // Check if this database accepts some extra characters
+ if ( extraNameChars.indexOf(ch) >= 0 ) {
+ return false;
+ }
+
+ return true;
+ }
+
+ private static boolean isUpperCase(char ch) {
+ return ch >= 'A' && ch <= 'Z';
+ }
+
+ private static boolean isLowerCase(char ch) {
+ return ch >= 'a' && ch <= 'z';
+ }
+
+ private static boolean isNumber(char ch) {
+ return ch >= '0' && ch <= '9';
+ }
+
+ private static boolean containsLowerCase(String identifier) {
+ int length = identifier.length();
+ for ( int i = 0 ; i < length ; i++ ) {
+ if ( isLowerCase(identifier.charAt(i)) ) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private static boolean containsUpperCase(String identifier) {
+
+ int length = identifier.length();
+ for ( int i = 0 ; i < length ; i++ ) {
+ if ( isUpperCase(identifier.charAt(i)) ) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private String getExtraNameChars(DatabaseMetaData dbmd) {
+ String chars = "";
+ try {
+ chars = dbmd.getExtraNameCharacters();
+ } catch ( SQLException e ) {
+ LOGGER.log(Level.WARNING, "DatabaseMetaData.getExtraNameCharacters()"
+ + " failed (" + e.getMessage() + "). " +
+ "Using standard set of characters");
+ LOGGER.log(Level.FINE, null, e);
+ }
+
+ return chars;
+ }
+
+ private String getQuoteString(DatabaseMetaData dbmd) {
+ String quoteStr = "\"";
+
+ try {
+ quoteStr = dbmd.getIdentifierQuoteString().trim();
+ } catch ( SQLException e ) {
+ LOGGER.log(Level.WARNING, "DatabaseMetaData.getIdentifierQuoteString()"
+ + " failed (" + e.getMessage() + "). " +
+ "Using '\"' for quoting SQL identifiers");
+ LOGGER.log(Level.FINE, null, e);
+ }
+
+ return quoteStr;
+ }
+
+ private int getCaseRule(DatabaseMetaData dbmd) {
+ int rule = UC_RULE;
+
+ try {
+ if ( dbmd.storesUpperCaseIdentifiers() ) {
+ rule = UC_RULE;
+ } else if ( dbmd.storesLowerCaseIdentifiers() ) {
+ rule = LC_RULE;
+ } else if ( dbmd.storesMixedCaseIdentifiers() ) {
+ rule = MC_RULE;
+ } else {
+ rule = UC_RULE;
+ }
+ } catch ( SQLException sqle ) {
+ LOGGER.log(Level.WARNING, "Exception trying to find out how " +
+ "the database stores unquoted identifiers, assuming " +
+ "upper case: " + sqle.getMessage());
+ LOGGER.log(Level.FINE, null, sqle);
+ }
+
+ return rule;
+ }
+ }
+
+}
Index: test/unit/src/org/netbeans/api/db/explorer/support/QuoterTest.java
===================================================================
RCS file: test/unit/src/org/netbeans/api/db/explorer/support/QuoterTest.java
diff -N test/unit/src/org/netbeans/api/db/explorer/support/QuoterTest.java
--- /dev/null 1 Jan 1970 00:00:00 -0000
+++ test/unit/src/org/netbeans/api/db/explorer/support/QuoterTest.java 11 Sep 2007 21:12:08 -0000
@@ -0,0 +1,120 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development
+ * and Distribution License (the License). You may not use this file except in
+ * compliance with the License.
+ *
+ * You can obtain a copy of the License at http://www.netbeans.org/cddl.html
+ * or http://www.netbeans.org/cddl.txt.
+ *
+ * When distributing Covered Code, include this CDDL Header Notice in each file
+ * and include the License file at http://www.netbeans.org/cddl.txt.
+ * If applicable, add the following below the CDDL Header, with the fields
+ * enclosed by brackets [] replaced by your own identifying information:
+ * "Portions Copyrighted [year] [name of copyright owner]"
+ *
+ * Portions Copyrighted 2007 Sun Microsystems, Inc.
+ */
+package org.netbeans.api.db.explorer.support;
+
+import org.netbeans.modules.db.util.DBTestBase;
+
+/**
+ * @author David Van Couvering
+ *
+ * This class is a set of tests to make sure we're quoting identifiers
+ * correctly
+ */
+public class QuoterTest extends DBTestBase {
+
+ private SQLIdentifiers.Quoter quoter;
+
+ public QuoterTest(String testName) {
+ super(testName);
+ }
+
+ public void setUp() throws Exception {
+ super.setUp();
+ quoter = SQLIdentifiers.createQuoter(conn.getMetaData());
+ }
+
+ public void testNoQuoting() {
+ String identifier = "YOUDONTNEEDTOQUOTEME2334252__1451";
+ String expResult = identifier;
+ String result = quoter.quoteIfNeeded(identifier);
+ assertEquals(expResult, result);
+ }
+
+ public void testSpaces() throws Exception {
+ String identifier = "YesYou Need to quote me";
+ String expResult = quote(identifier);
+
+ String result = quoter.quoteIfNeeded(identifier);
+
+ assertEquals(expResult, result);
+ }
+
+ public void testCasing() throws Exception {
+ String identifier;
+
+ // First, find out what kind of casing is done with unquoted
+ // identifiers for this connection
+ int caseRule = getUnquotedCaseRule();
+
+ switch (caseRule) {
+ case LC_RULE:
+ identifier = "ABCDEFG";
+ break;
+ case UC_RULE:
+ identifier = "abcdefg";
+ break;
+ default:
+ // Nothing to test here
+ return;
+ }
+
+ String expResult = quote(identifier);
+
+ String result = quoter.quoteIfNeeded(identifier);
+
+ assertEquals(expResult, result);
+ }
+
+ public void testNonAscii() throws Exception {
+ // borrowed translated message from Derby message file :)
+ String identifier = "abcdABCD0934" +
+ "\u4f8b\u5916\u306e\u305f\u3081\u3001\u59cb\u52d5" +
+ "\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002 \u8a73\u7d30" +
+ "\u306b\u3064\u3044\u3066\u306f\u3001\u6b21\u306e\u4f8b\u5916" +
+ "\u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002" +
+ "09298719871";
+
+ String expResult = quote(identifier);
+
+ String result = quoter.quoteIfNeeded(identifier);
+
+ assertEquals(expResult, result);
+ }
+
+ public void testDontQuoteQuoted() throws Exception {
+ String identifier = quote("I am already quoted");
+
+ String expResult = identifier;
+
+ String result = quoter.quoteIfNeeded(identifier);
+
+ assertEquals(expResult, result);
+ }
+
+ public void testNullIdentifier() throws Exception {
+ boolean gotException = false;
+ try {
+ quoter.quoteIfNeeded(null);
+ } catch ( NullPointerException npe ) {
+ // expected
+ gotException = true;
+ }
+
+ assertTrue(gotException);
+ }
+
+}
Index: test/unit/src/org/netbeans/modules/db/util/DBTestBase.java
===================================================================
RCS file: /cvs/db/test/unit/src/org/netbeans/modules/db/util/DBTestBase.java,v
retrieving revision 1.2
diff -u -u -r1.2 DBTestBase.java
--- test/unit/src/org/netbeans/modules/db/util/DBTestBase.java 31 Aug 2007 17:48:12 -0000 1.2
+++ test/unit/src/org/netbeans/modules/db/util/DBTestBase.java 11 Sep 2007 21:12:08 -0000
@@ -78,8 +78,8 @@
public static final int MC_RULE = 2; // mixed case remains mixed case
public static final int QUOTE_RETAINS_CASE = 3; // quoted idents retain case
- private static int identRule = RULE_UNDEFINED;
- private static int quotedIdentRule = RULE_UNDEFINED;
+ private static int unquotedCaseRule = RULE_UNDEFINED;
+ private static int quotedCaseRule = RULE_UNDEFINED;
protected static SpecificationFactory specfactory;
@@ -442,12 +442,12 @@
* require identifiers to be in proper case
*/
public String fixIdentifier(String ident) throws Exception {
- if ( identRule == RULE_UNDEFINED ) {
- getIdentRules();
+ if ( unquotedCaseRule == RULE_UNDEFINED ) {
+ getCaseRules();
}
if ( isQuoted(ident) ) {
- switch ( quotedIdentRule ) {
+ switch ( quotedCaseRule ) {
case QUOTE_RETAINS_CASE:
break;
case UC_RULE:
@@ -460,12 +460,12 @@
break;
default:
LOGGER.log(Level.WARNING, "Unexpected identifier rule: +" +
- identRule + ", assuming case is retained");
+ unquotedCaseRule + ", assuming case is retained");
}
return ident.substring(1, ident.length() -1);
} else {
- switch ( identRule ) {
+ switch ( unquotedCaseRule ) {
case UC_RULE:
return ident.toUpperCase();
case LC_RULE:
@@ -474,7 +474,7 @@
return ident;
default:
LOGGER.log(Level.WARNING, "Unexpected identifier rule: +" +
- identRule + ", assuming upper case");
+ unquotedCaseRule + ", assuming upper case");
return ident.toUpperCase();
}
}
@@ -486,7 +486,12 @@
return ident.startsWith(quoteString) && ident.endsWith(quoteString);
}
- private void getIdentRules() throws Exception {
+ public int getUnquotedCaseRule() throws Exception {
+ getCaseRules();
+ return unquotedCaseRule;
+ }
+
+ private void getCaseRules() throws Exception {
assert conn != null;
DatabaseMetaData md;
@@ -494,13 +499,13 @@
try {
md = conn.getMetaData();
if ( md.storesUpperCaseIdentifiers() ) {
- identRule = UC_RULE;
+ unquotedCaseRule = UC_RULE;
} else if ( md.storesLowerCaseIdentifiers() ) {
- identRule = LC_RULE;
+ unquotedCaseRule = LC_RULE;
} else if ( md.storesMixedCaseIdentifiers() ) {
- identRule = MC_RULE;
+ unquotedCaseRule = MC_RULE;
} else {
- identRule = UC_RULE;
+ unquotedCaseRule = UC_RULE;
}
} catch ( SQLException sqle ) {
LOGGER.log(Level.INFO, "Exception trying to find out how " +
@@ -508,20 +513,20 @@
sqle.getMessage());
LOGGER.log(Level.FINE, null, sqle);
- identRule = UC_RULE;
+ unquotedCaseRule = UC_RULE;
}
try {
md = conn.getMetaData();
if ( md.storesLowerCaseQuotedIdentifiers() ) {
- quotedIdentRule = LC_RULE;
+ quotedCaseRule = LC_RULE;
} else if ( md.storesUpperCaseQuotedIdentifiers() ) {
- quotedIdentRule = UC_RULE;
+ quotedCaseRule = UC_RULE;
} else if ( md.storesMixedCaseQuotedIdentifiers() ) {
- quotedIdentRule = MC_RULE;
+ quotedCaseRule = MC_RULE;
} else {
- quotedIdentRule = QUOTE_RETAINS_CASE;
+ quotedCaseRule = QUOTE_RETAINS_CASE;
}
} catch ( SQLException sqle ) {
LOGGER.log(Level.INFO, "Exception trying to find out how " +
@@ -529,7 +534,7 @@
sqle.getMessage());
LOGGER.log(Level.FINE, null, sqle);
- quotedIdentRule = QUOTE_RETAINS_CASE;
+ quotedCaseRule = QUOTE_RETAINS_CASE;
}
}