blob: a4f20ba624c4afcccd3f098463f01a0845eedc43 [file] [log] [blame]
/*
* 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.modules.db.dataview.output;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Helper class for extracting error positions out of SQLExceptions.
*/
public class ErrorPositionExtractor {
private static final Logger LOG = Logger.getLogger(ErrorPositionExtractor.class.getName());
private ErrorPositionExtractor() {
}
/**
* Extract error location from the supplied input.
*
* <p>
* Some DB vendors supply position information if errors are encountered.
* What and how this is supported depends on the vendor. This class bundles
* the approaches and offers a central place for the implementation.</p>
*
* <p>
* For DB specific details please see the corresponding implementation.</p>
*
* @param con Connection that was in use or NULL if not available
* @param stmt Statement that was in use or NULL if not available
* @param ex Exception that was recorded an should be analysed
* @param sql The SQL that was executed
* @return integer describing the position as a zero-based offset into the
* supplied sql
*/
public static int extractErrorPosition(Connection con, Statement stmt, Throwable ex, String sql) {
try {
if (ex == null || con == null) {
return -1;
} else if (con.getMetaData().getDriverName().toLowerCase().contains("postgresql")) {
return extractErrorPositionForPostgresql(con, stmt, ex, sql);
} else if (con.getMetaData().getDriverName().toLowerCase().contains("informix")) {
return extractErrorPositionForInformix(con, stmt, ex, sql);
} else if (con.getMetaData().getDriverName().toLowerCase().contains("derby")) {
return extractErrorPositionForDerby(con, stmt, ex, sql);
} else if (con.getMetaData().getDriverName().toLowerCase().contains("h2 jdbc")) {
return extractErrorPositionForH2(con, stmt, ex, sql);
} else {
return -1;
}
} catch (Exception innerEx) {
LOG.log(Level.FINE, "Failed to extract ErrorPosition", innerEx);
return -1;
}
}
/**
* Extract location information for PostgreSQL
*
* <p>
* PostgreSQL derives its exception from java.sql.SQLException and adds a
* ServerErrorMessage. This messages contains also the position.</p>
*
* <p>
* In this case the data is extracted via reflection.</p>
*/
private static int extractErrorPositionForPostgresql(Connection con, Statement stmt, Throwable ex, String sql) {
if (ex == null) {
return -1;
}
Class exceptionClass = ex.getClass();
if (exceptionClass.getName().equals("org.postgresql.util.PSQLException")) {
try {
Method getServerErrorMessage = exceptionClass.getMethod("getServerErrorMessage");
Object serverErrorMessage = getServerErrorMessage.invoke(ex);
Class messageClass = serverErrorMessage.getClass();
Method getPosition = messageClass.getMethod("getPosition");
Integer result = (Integer) getPosition.invoke(serverErrorMessage);
if (result != null && result > 0) {
return result - 1;
} else {
return -1;
}
} catch (NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException | InvocationTargetException | NullPointerException innerEx) {
LOG.log(Level.FINE, "Failed to parse PostgreSQL error", innerEx);
return -1;
}
} else {
LOG.log(Level.FINE, "Caught PostgreSQL exception, that is not subclass of PSQLException", ex);
return -1;
}
}
/**
* Extract location information for Informix DB.
*
* <p>
* For Informix the location information is not present in the exception
* itself. It has to be extracted from the connection.</p>
*
* <p>
* In this case the connection is accessed via reflection to call the
* corresponding method.</p>
*
* <p>
* In case connection gets wrapped later it needs to be unwrapped here.</p>
*/
private static int extractErrorPositionForInformix(Connection con, Statement stmt, Throwable ex, String sql) {
// try to get exact position from exception message
if (ex == null) {
return -1;
}
Class connectionClass = con.getClass();
if (connectionClass.getName().startsWith("com.informix.jdbc")) {
try {
Method getSQLStatementOffset = connectionClass.getMethod("getSQLStatementOffset");
int column = (Integer) getSQLStatementOffset.invoke(con);
if (column <= 0) {
return -1;
} else {
return column - 1;
}
} catch (NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException | InvocationTargetException innerEx) {
LOG.log(Level.FINE, "Failed to extract informix error location", innerEx);
return -1;
}
} else {
return -1;
}
}
/**
* Extract location information for Derby DB.
*
* <p>
* Based on the pattern '{@code at line LINE, column COLUMN.}' the message
* is analysed and the resulting position is returned.</p>
*/
private static final Pattern positionPatternDerby = Pattern.compile("at line (\\d+), column (\\d+)");
private static int extractErrorPositionForDerby(Connection con, Statement stmt, Throwable ex, String sql) {
if (!(ex instanceof SQLException)) {
return -1;
}
SQLException se = (SQLException) ex;
String msg = se.getMessage();
Matcher matcher = positionPatternDerby.matcher(msg);
if (matcher.find()) {
int line = Integer.parseInt(matcher.group(1));
int lineOffset = 0;
int column = Integer.parseInt(matcher.group(2)) - 1;
for (int toSkip = line - 1; toSkip > 0; toSkip--) {
lineOffset = sql.indexOf("\n", lineOffset) + 1;
}
return lineOffset + column;
}
return -1;
}
/**
* Extract location information for H2 DB.
*
* <p>
* This is a hybrid - error location can only be extracted for the
* errorcodes:</p>
*
* <dl>
* <dt>42000</dt>
* <dd>SYNTAX_ERROR_1 - he error with code 42000 is thrown when trying to
* execute an invalid SQL statement. (Messageformat: '{@code Syntax error in SQL statement {0}}')</dd>
* <dt>42001</dt>
* <dd>SYNTAX_ERROR_2 - The error with code 42001 is thrown when trying to
* execute an invalid SQL statement. (Messageformat: '{@code Syntax error in SQL statement {0}; expected {1}}')</dd>
* </dl>
*
* The error messages in H2 always contain the english localised version,
* so string parsing can work. The sql in the message is identifier quoted.
*/
private static final Pattern h2SyntaxPattern = Pattern.compile("Syntax error in SQL statement \"((([^\"])|(\"\"))*)\"");
private static int extractErrorPositionForH2(Connection con, Statement stmt, Throwable ex, String sql) {
if (!(ex instanceof SQLException)) {
return -1;
}
SQLException se = (SQLException) ex;
Matcher matcher;
if(se.getErrorCode() == 42000 || se.getErrorCode() == 42001) {
matcher = h2SyntaxPattern.matcher(se.getMessage());
} else {
return -1;
}
if (matcher.find()) {
String errorSQL = matcher.group(1).replace("\"\"", "\"");
String lowerReference = sql.toLowerCase();
String lowerError = errorSQL.toLowerCase();
int endIdx = Math.min(lowerReference.length(), lowerError.length());
for(int i = 0; i < endIdx && i < (lowerError.length() - 3); i++) {
if(lowerReference.charAt(i) != lowerError.charAt(i)) {
if("[*]".equals(errorSQL.substring(i, i + 3))) {
return i;
} else {
return -1;
}
}
}
// Corner case: lastIdx is the problem => detect it
if(lowerError.length() >= (endIdx + 2)) {
if(lowerError.startsWith("[*]", endIdx)) {
return endIdx;
}
}
return -1;
}
return -1;
}
}