blob: 476a06f3b2f950f769a914eecec731ed70e9fae2 [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.apache.empire.db;
import java.sql.Timestamp;
import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.Collection;
import java.util.Date;
import org.apache.empire.commons.ObjectUtils;
import org.apache.empire.commons.OptionEntry;
import org.apache.empire.commons.StringUtils;
import org.apache.empire.data.DataType;
import org.apache.empire.dbms.DBMSHandler;
import org.apache.empire.dbms.DBSqlPhrase;
import org.apache.empire.exceptions.InvalidArgumentException;
import org.apache.empire.exceptions.NotSupportedException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* DBSQLBuilder
* This class is used for building a single SQL statement
* @author doebele
*/
public abstract class DBSQLBuilder implements Appendable
{
private static final Logger log = LoggerFactory.getLogger(DBSQLBuilder.class);
protected static final char TEXT_DELIMITER = '\'';
protected final DBMSHandler dbms;
protected final StringBuilder sql = new StringBuilder(64);
protected DBCmdParamList cmdParamList;
/**
* Don't use this directly
* Use dbms.createSQLBuilder()
*/
protected DBSQLBuilder(DBMSHandler dbms)
{
this.dbms = dbms;
}
public void setCmdParams(DBCmdParamList cmdParamList)
{
this.cmdParamList = cmdParamList;
}
/**
* returns the SQL as a String
*/
@Override
public String toString()
{
return sql.toString();
}
/*
* getters
*/
public DBMSHandler getDbms()
{
return dbms;
}
public String getPhrase(DBSqlPhrase phrase)
{
return dbms.getSQLPhrase(phrase);
}
/*
* special
*/
public int length()
{
return sql.length();
}
public void reset(int pos)
{
if (pos>sql.length())
throw new InvalidArgumentException("pos", pos);
sql.setLength(pos);
}
/*
* appenders
*/
@Override
public DBSQLBuilder append(CharSequence sqlLiteral)
{
sql.append(sqlLiteral);
return this;
}
@Override
public DBSQLBuilder append(CharSequence sqlLiteral, int start, int end)
{
sql.append(sqlLiteral, start, end);
return this;
}
@Override
public DBSQLBuilder append(char c)
{
sql.append(c);
return this;
}
public DBSQLBuilder append(long l) {
sql.append(l);
return this;
}
public DBSQLBuilder append(double d) {
sql.append(d);
return this;
}
public DBSQLBuilder append(DBSqlPhrase phrase)
{
sql.append(dbms.getSQLPhrase(phrase));
return this;
}
public void append(DBCommandExpr subQueryCmd)
{
// append select
sql.append(subQueryCmd.getSelect());
// check params
DBCmdParams params = subQueryCmd.getParams();
if (params.isEmpty())
return;
// cmdParamList
if (cmdParamList==null)
throw new NotSupportedException(this, "append command with params");
// merge
cmdParamList.mergeSubqueryParams(params);
}
/**
* Appends the SQL representation of a value
* @param type the data type
* @param value an DBExpr object, array or a basis data type(e.g. int, String)
*/
public void appendValue(DataType type, Object value)
{
appendValue(type, value, DBExpr.CTX_DEFAULT, "+");
}
/**
* Appends the SQL representation of a value
* @param dataType the DataType
* @param value an DBExpr object, array or a basis data type(e.g. int, String)
* @param context the context of the DBColumnExpr object
* @param arraySep the separator value
*/
public void appendValue(DataType dataType, Object value, long context, String arraySep)
{
// it's an Object
if (value instanceof DBExpr)
{ // it's an expression
((DBExpr) value).addSQL(this, context);
return;
}
// check option entry
if (value instanceof OptionEntry)
{ // option value
value = ((OptionEntry)value).getValue();
}
// check enum
if (value instanceof Enum<?>)
{ // check enum
value = ObjectUtils.getEnumValue((Enum<?>)value, dataType.isNumeric());
}
else if (value instanceof Collection<?>)
{ // collection 2 array
value = ((Collection<?>)value).toArray();
}
// Check whether it is an array
if (value!=null && value.getClass().isArray())
{ // An Array of Objects
Object[] array = (Object[]) value;
for (int i = 0; i < array.length; i++)
{ // Array Separator
if (i > 0 && arraySep != null)
sql.append(arraySep);
// Append Value
appendValue(dataType, array[i], context, arraySep);
}
return;
}
else
{ // Get Value Expression from dmbs
appendSimpleValue(dataType, value);
}
}
/**
* Expands an SQL template and adds it to the SQL command
*
* @param template the SQL template
* @param values list of values to be inserted into the template
* @param dataTypes list of data types
* @param context the context of the DBColumnExpr object
* @param arraySep the separator value
*/
public void appendTemplate(String template, Object[] values, DataType[] dataTypes, long context, String arraySep)
{
int pos = 0;
while (true)
{
// find begin
int beg = template.indexOf('{', pos);
if (beg < 0)
break;
// append
sql.append(template.substring(pos, beg));
// find end
int end = template.indexOf('}', ++beg);
if (end < 0)
throw new InvalidArgumentException("template", template);
// part
int iParam = Integer.parseInt(template.substring(beg, end));
if (iParam<0 || iParam>=values.length)
throw new InvalidArgumentException("params", values);
// add value
DataType dataType = (dataTypes.length>=iParam ? dataTypes[iParam] : dataTypes[0]);
appendValue(dataType, values[iParam], context, arraySep);
// next
pos = end + 1;
}
if (pos < template.length())
{ // add the rest
sql.append(template.substring(pos));
// special case: Nothing added yet
if (pos==0 && values!=null && values.length>0)
log.warn("No Placeholder for found in template {}!", template);
}
}
/*
* internal
*/
/**
* Returns a sql string for a given value.
* Text will be enclosed in single quotes and existing single quotes will be doubled.
* Empty strings are treated as null.
* The syntax of Date, Datetime and Boolean values are DBMS specific.
*
* @param type the data type of the supplied value
* @param value the value which is inserted to the new String
*/
protected void appendSimpleValue(DataType type, Object value)
{
if (value instanceof Enum<?>)
{ // convert enum
log.warn("Enum of type {} supplied for getValueString. Converting value...", value.getClass().getName());
value = ObjectUtils.getEnumValue((Enum<?>)value, type.isNumeric());
}
if (ObjectUtils.isEmpty(value))
{ // null
append(DBSqlPhrase.SQL_NULL);
return;
}
// set string buffer
switch (type)
{
case DATE:
sql.append(getDateTimeString(value, DBSqlPhrase.SQL_DATE_TEMPLATE, DBSqlPhrase.SQL_DATE_PATTERN, DBSqlPhrase.SQL_CURRENT_DATE));
return;
case TIME:
sql.append(getDateTimeString(value, DBSqlPhrase.SQL_TIME_TEMPLATE, DBSqlPhrase.SQL_TIME_PATTERN, DBSqlPhrase.SQL_CURRENT_TIME));
return;
case DATETIME:
String text;
// Only date (without time) provided?
if (!DBDatabase.SYSDATE.equals(value) && !(value instanceof Date) && ObjectUtils.lengthOf(value)<=10)
text = getDateTimeString(value, DBSqlPhrase.SQL_DATE_TEMPLATE, DBSqlPhrase.SQL_DATE_PATTERN, DBSqlPhrase.SQL_CURRENT_TIMESTAMP);
else // Complete Date-Time Object with time
text = getDateTimeString(value, DBSqlPhrase.SQL_DATETIME_TEMPLATE, DBSqlPhrase.SQL_DATETIME_PATTERN, DBSqlPhrase.SQL_CURRENT_TIMESTAMP);
sql.append(text);
return;
case TIMESTAMP:
sql.append(getDateTimeString(value, DBSqlPhrase.SQL_TIMESTAMP_TEMPLATE, DBSqlPhrase.SQL_TIMESTAMP_PATTERN, DBSqlPhrase.SQL_CURRENT_TIMESTAMP));
return;
case VARCHAR:
case CHAR:
case CLOB:
case UNIQUEID:
appendStringLiteral(type, value);
return;
case BOOL:
// Get Boolean value
boolean boolVal = false;
if (value instanceof Boolean)
{ boolVal = ((Boolean) value).booleanValue();
}
else
{ // Boolean from String
boolVal = stringToBoolean(value.toString());
}
append((boolVal) ? DBSqlPhrase.SQL_BOOLEAN_TRUE : DBSqlPhrase.SQL_BOOLEAN_FALSE);
return;
case INTEGER:
case DECIMAL:
case FLOAT:
sql.append(getNumberString(value, type));
return;
case BLOB:
throw new NotSupportedException(this, "appendSimpleValue(DataType.BLOB)");
case AUTOINC:
case UNKNOWN:
/* Allow expressions */
sql.append(value.toString());
return;
default:
log.warn("Unknown DataType {} for getValueString().", type);
sql.append(value.toString());
}
}
/**
* encodes a Date value for an SQL command string.
* @param value the value to encode
* @param sqlTemplate the template
* @param sqlPattern the pattern
* @param sqlCurrentDate the current date phrase
* @return the date time string
*/
protected String getDateTimeString(Object value, DBSqlPhrase sqlTemplate, DBSqlPhrase sqlPattern, DBSqlPhrase sqlCurrentDate)
{
// is it a sysdate expression
if (DBDatabase.SYSDATE.equals(value))
return dbms.getSQLPhrase(sqlCurrentDate);
// Format the date (ymd)
Timestamp ts;
if ((value instanceof Timestamp))
{ // We have a timestamp
ts = (Timestamp)value;
}
else if ((value instanceof Date))
{ // Convert Date to Timestamp
ts = new Timestamp(((Date)value).getTime());
}
else if ((value instanceof LocalDate))
{ // Convert LocalDate to Timestamp
ts = java.sql.Timestamp.valueOf(((LocalDate)value).atStartOfDay());
}
else if ((value instanceof LocalDateTime))
{ // Convert LocalDateTime to Timestamp
ts = java.sql.Timestamp.valueOf((LocalDateTime)value);
}
else
{ // "Timestamp format must be yyyy-mm-dd hh:mm:ss[.fffffffff]"
String dtValue = value.toString().trim();
try
{ // parse timestamp
ts = Timestamp.valueOf(dtValue);
} catch (Throwable e) {
// Invalid date
log.error("Unable to parse date value "+dtValue, e);
throw new InvalidArgumentException("value", value);
}
}
// Convert to String
String pattern = dbms.getSQLPhrase(sqlPattern);
SimpleDateFormat sqlFormat = new SimpleDateFormat(dbms.getSQLPhrase(sqlPattern));
String datetime = sqlFormat.format(ts);
// Add micro / nanoseconds
int nanos = (ts.getNanos() % 1000000);
if (pattern.endsWith(".SSS") && nanos>0)
{ // Add nanoseconds
if (((nanos) % 100)>0)
datetime += String.format("%06d", nanos);
else
datetime += String.format("%04d",(nanos/100));
}
// Now Build String
String template = dbms.getSQLPhrase(sqlTemplate);
return StringUtils.replace(template, "{0}", datetime);
}
/**
* encodes Text values for an SQL command string.
* @param type date type (can only be TEXT, CHAR, CLOB and UNIQUEID)
* @param value the literal to be encoded
*/
protected void appendStringLiteral(DataType type, Object value)
{ // text
if (value==null)
{ append(DBSqlPhrase.SQL_NULL);
return;
}
String text = value.toString();
sql.append(TEXT_DELIMITER);
if (DBDatabase.EMPTY_STRING.equals(text)==false)
escapeAndAppendLiteral(text);
sql.append(TEXT_DELIMITER);
}
/**
* this helper function doubles up single quotes for SQL
*/
protected void escapeAndAppendLiteral(String value)
{
int pos = 0;
int delim;
// find delimiter
while ((delim = value.indexOf(TEXT_DELIMITER, pos))>=0)
{ // append
if (delim>pos)
sql.append(value.substring(pos, delim));
// double up
sql.append("''");
// next
pos = delim + 1;
}
if (pos==0)
sql.append(value); // add entire string
else if (pos < value.length())
sql.append(value.substring(pos)); // add the rest
}
/**
* encodes a numeric value for an SQL command string.
* @param value the numeric value
* @param type the number data type
* @return the string representation of the number
*/
protected String getNumberString(Object value, DataType type)
{
// already a number
if (value instanceof Number)
return value.toString();
// check if it is a number
String s = value.toString();
boolean integerOnly = (type==DataType.INTEGER);
for (int i=0; i<s.length(); i++)
{
char c = s.charAt(i);
if (c>='0' && c<='9')
continue; // OK
if (c=='-' || c=='+')
continue; // OK
if (c==' ' && i>0)
return s.substring(0,i);
// check
if (integerOnly || (c!='.' && c!=','))
throw new NumberFormatException(s);
}
return s;
}
/**
* this function converts a string containing a boolean expression to a boolean.
* @param value the string containing a boolean expression
* @return true if the string contains either "true", "y" or "1" or false otherwise
*/
protected boolean stringToBoolean(final String value)
{
return "1".equals(value) ||
"true".equalsIgnoreCase(value) ||
"y".equalsIgnoreCase(value);
}
}