/**
* 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.apache.jena.jdbc;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Types;
import org.apache.jena.graph.Node ;
import org.apache.jena.jdbc.results.metadata.columns.BooleanColumn;
import org.apache.jena.jdbc.results.metadata.columns.ByteColumn;
import org.apache.jena.jdbc.results.metadata.columns.ColumnInfo;
import org.apache.jena.jdbc.results.metadata.columns.DateColumn;
import org.apache.jena.jdbc.results.metadata.columns.DateTimeColumn;
import org.apache.jena.jdbc.results.metadata.columns.DecimalColumn;
import org.apache.jena.jdbc.results.metadata.columns.DoubleColumn;
import org.apache.jena.jdbc.results.metadata.columns.FloatColumn;
import org.apache.jena.jdbc.results.metadata.columns.LongIntegerColumn;
import org.apache.jena.jdbc.results.metadata.columns.IntegerColumn;
import org.apache.jena.jdbc.results.metadata.columns.StringColumn;
import org.apache.jena.jdbc.results.metadata.columns.TimeColumn;
import org.apache.jena.vocabulary.XSD ;
/**
* <p>
* Class containing constants and helper methods related to JDBC compatibility
* </p>
* <h3>Understanding Compatibility Levels</h3>
* <p>
* Since JDBC is very SQL centric API by definition shoe-horning SPARQL into it
* has some caveats and provisos, the aim of this class it to provide some level
* of configurability of how nice we will try to play with JDBC. We provide the
* notion of a configurable compatibility level, by definition we use the
* {@link #MEDIUM} compatibility level, see the documentation on the constants
* and helper methods to understand exactly what each level means.
* </p>
*
*/
public class JdbcCompatibility {
/**
* Private constructor prevents instantiation
*/
private JdbcCompatibility() {
}
/**
* Constant for low JDBC compatibility level
* <p>
* This is the level you should use when you know you are accessing a SPARQL
* source and are able to cope with the Jena/ARQ representation of RDF terms
* natively.
* </p>
* <h3>Behavior Specifies</h3>
* <ul>
* <li>Column Typing - All result set columns are reported as being as typed
* as {@link Types#JAVA_OBJECT} and Java type is the ARQ {@link Node} type.</li>
* </ul>
*/
public static final int LOW = 1;
/**
* Constant for medium JDBC compatibility level
* <p>
* This is the default compatibility level, we will make some effort to be
* compatible with JDBC but these efforts will not be perfect.
* </p>
* <h3>Behavior Specifics</h3>
* <ul>
* <li>Column Typing - All result set columns are reported as being as typed
* as {@link Types#NVARCHAR} and Java type is {@link String}.</li>
* </ul>
*/
public static final int MEDIUM = 5;
/**
* Constant for high JDBC compatibility level
* <p>
* This is the highest compatibility level, we will do our best to be
* compatible with JDBC however these efforts may still not be perfect.
* </p>
* <h3>Behavior Specifics</h3>
* <ul>
* <li>Column Typing - Result set columns are typed by inspecting the first
* row of the data so native JDBC types like {@link Types#INTEGER} and so
* forth may be reported depending on the query.</li>
* </ul>
*/
public static final int HIGH = 9;
/**
* Constant for default JDBC compatibility which is set to {@link #MEDIUM}
*/
public static final int DEFAULT = MEDIUM;
/**
* Normalizes the compatibility level given to be within the acceptable
* range of 1-9
*
* @param level
* Level
* @return Normalized level
*/
public static int normalizeLevel(int level) {
if (level < LOW) {
return LOW;
} else if (level > HIGH) {
return HIGH;
} else {
return level;
}
}
/**
* Returns whether a result set is expected to determine the column types
* from the returned data
*
* @param level
* Desired compatibility level
* @return True if column types should be detected, false otherwise
*/
public static boolean shouldDetectColumnTypes(int level) {
level = normalizeLevel(level);
return level == HIGH;
}
/**
* Returns whether a result set is expected to type returned columns as
* strings
*
* @param level
* Desired compatibility level
* @return True if columns should be typed as string, false otherwise
*/
public static boolean shouldTypeColumnsAsString(int level) {
level = normalizeLevel(level);
return level >= MEDIUM && level < HIGH;
}
/**
* Detects the column type information based on an example value
*
* @param var
* Variable Name i.e. the column label
* @param value
* Example value
* @param allowsNulls
* Whether the result set we are detecting the type for allows
* null values in this column
* @return Column Information
* @throws SQLException
* Thrown if the column type cannot be detected, this should
* only occur if you state that the column does not allow nulls
* and then provide a null example value
*/
public static ColumnInfo detectColumnType(String var, Node value, boolean allowsNulls) throws SQLException {
if (allowsNulls && value == null) {
// If we are allowing nulls and the value is null just type the
// column as string
return new StringColumn(var, ResultSetMetaData.columnNullable);
} else if (!allowsNulls && value == null) {
throw new SQLException("Unable to determine column type, column is non-nullable but example value is null");
} else {
// We know we have a non-null value so now we need to determine the
// column type appropriately
int nullable = allowsNulls ? ResultSetMetaData.columnNullable : ResultSetMetaData.columnNoNulls;
if (value.isBlank()) {
// Type blank nodes as strings
return new StringColumn(var, nullable);
} else if (value.isURI()) {
// Type URIs as strings
// TODO: Does JDBC have a URL type?
return new StringColumn(var, nullable);
} else if (value.isLiteral()) {
// Literals will be typed based on the declared data type where
// applicable
String dtUri = value.getLiteralDatatypeURI();
if (dtUri != null) {
// Is a typed literal
return selectColumnType(var, dtUri, nullable);
} else {
// Untyped literals are typed as strings
return new StringColumn(var, nullable);
}
} else {
// Anything else we treat as a string
return new StringColumn(var, nullable);
}
}
}
/**
* Select the column type based on the data type URI
*
* @param var
* Variable Name i.e. the column label
* @param dtUri
* Data type URI
* @param nullable
* Whether the column is nullable
* @return Column type
* @throws SQLException
*/
private static ColumnInfo selectColumnType(String var, String dtUri, int nullable) throws SQLException {
if (dtUri.equals(XSD.date.toString())) {
// Date column
return new DateColumn(var, nullable);
} else if (dtUri.equals(XSD.dateTime.toString())) {
// Date time column
return new DateTimeColumn(var, nullable);
} else if (dtUri.equals(XSD.decimal.toString())) {
// Decimal column
return new DecimalColumn(var, nullable);
} else if (dtUri.equals(XSD.duration.toString())) {
// JDBC has no notion of durations so return as a string
return new StringColumn(var, nullable);
} else if (dtUri.equals(XSD.integer.toString()) || dtUri.equals(XSD.xint.toString())
|| dtUri.equals(XSD.xlong.toString())) {
// Integer column
return new LongIntegerColumn(var, nullable, true);
} else if (dtUri.equals(XSD.unsignedInt.toString()) || dtUri.equals(XSD.unsignedLong.toString())) {
// Unsigned Integer column
return new LongIntegerColumn(var, nullable, false);
} else if (dtUri.equals(XSD.positiveInteger.toString()) || dtUri.equals(XSD.nonNegativeInteger.toString())) {
// Unsigned Integer column
return new LongIntegerColumn(var, nullable, false);
} else if (dtUri.equals(XSD.nonPositiveInteger.toString()) || dtUri.equals(XSD.negativeInteger.toString())) {
// Signed Integer column
return new LongIntegerColumn(var, nullable, true);
} else if (dtUri.equals(XSD.xshort.toString())) {
// Short Integer column
return new IntegerColumn(var, nullable, true);
} else if (dtUri.equals(XSD.unsignedShort.toString())) {
// Unsigned Short Integer column
return new IntegerColumn(var, nullable, false);
} else if (dtUri.equals(XSD.xbyte.toString())) {
// Signed Byte
return new ByteColumn(var, nullable, true);
} else if (dtUri.equals(XSD.unsignedByte.toString())) {
// Unsigned Byte
return new ByteColumn(var, nullable, false);
} else if (dtUri.equals(XSD.time.toString())) {
// Time column
return new TimeColumn(var, nullable);
} else if (dtUri.equals(XSD.xboolean.toString())) {
// Boolean column
return new BooleanColumn(var, nullable);
} else if (dtUri.equals(XSD.xdouble.toString())) {
// Double column
return new DoubleColumn(var, nullable);
} else if (dtUri.equals(XSD.xfloat.toString())) {
// Float column
return new FloatColumn(var, nullable);
} else if (dtUri.equals(XSD.xstring.toString())) {
// String column
return new StringColumn(var, nullable);
} else {
// Anything else we'll treat as a String
return new StringColumn(var, nullable);
}
}
/**
* Parses the JDBC compatibility level from the given object and normalizes
* it if necessary. If the given object is null or does not contain a valid
* integer value (or can be parsed as such) then the returned compatibility
* level will be {@link #DEFAULT}
*
* @param object
* Object to parse
* @return Normalized JDBC compatibility level
*/
public static int parseLevel(Object object) {
if (object == null) {
return DEFAULT;
} else if (object instanceof Integer) {
return normalizeLevel((Integer) object);
} else {
try {
int level = Integer.parseInt(object.toString());
return normalizeLevel(level);
} catch (NumberFormatException e) {
return DEFAULT;
}
}
}
}