001package io.ebean.config.dbplatform;
002
003import java.sql.Types;
004import java.util.LinkedHashMap;
005import java.util.Map;
006
007import javax.xml.bind.DatatypeConverter;
008
009import io.ebean.annotation.DbDefault;
010
011/**
012 * DB Column default values mapping to database platform specific literals.
013 */
014public class DbDefaultValue {
015
016  /**
017   * The key for FALSE.
018   */
019  public static final String FALSE = "false";
020
021  /**
022   * The key for TRUE.
023   */
024  public static final String TRUE = "true";
025
026  /**
027   * The key for the NOW / current timestamp.
028   */
029  public static final String NOW = "now";
030  
031  /**
032   * The 'null' literal.
033   */
034  public static final String NULL = "null";
035  
036
037
038  protected Map<String, String> map = new LinkedHashMap<>();
039
040  /**
041   * Set the DB now function.
042   */
043  public void setNow(String dbFunction) {
044    put(NOW, dbFunction);
045  }
046
047  /**
048   * Set the DB false literal.
049   */
050  public void setFalse(String dbFalseLiteral) {
051    put(FALSE, dbFalseLiteral);
052  }
053
054  /**
055   * Set the DB true literal.
056   */
057  public void setTrue(String dbTrueLiteral) {
058    put(TRUE, dbTrueLiteral);
059  }
060
061  /**
062   * Add an translation entry.
063   */
064  public void put(String dbLiteral, String dbTranslated) {
065    map.put(dbLiteral, dbTranslated);
066  }
067
068  /**
069   * Convert the DB default literal to platform specific type or function.
070   * <p>
071   * This is intended for the DB column default clause in DDL.
072   * </p>
073   */
074  public String convert(String dbDefaultLiteral) {
075    if (dbDefaultLiteral == null) {
076      return null;
077    }
078    if (dbDefaultLiteral.startsWith("$RAW:")) {
079      return dbDefaultLiteral.substring(5);
080    }
081    String val = map.get(dbDefaultLiteral);
082    return val != null ? val : dbDefaultLiteral;
083  }
084
085  
086  /**
087   * This method checks & convert the {@link DbDefault#value()} to a valid SQL literal.
088   * 
089   * This is mainly to quote string literals and verify integer/dates for correctness.
090   * <p>
091   * Note: There are some special cases:
092   * </p>
093   * <ul>
094   *    <li>Normal Quoting: <code>@DbDefault("User's default")</code> on a String propery
095   *        returns: <code>default 'User''s default'</code><br/>
096   *        (the same on an integer property will throw a NumberFormatException)</li>
097   *    <li>Special case null: <code>@DbDefault("null")</code> will return this: <code>default null</code><br/>
098   *        If you need really the String "null", you have to specify <code>@DbDefault("'null'")</code>
099   *        which gives you the <code>default 'null'</code> statement.</li>
100   *    <li>Any statement, that begins and ends with single quote will not be checked or get quoted again.</li>
101   *    <li>A statement that begins with "$RAW:", e.g <code>@DbDefault("$RAW:N'SANDNES'")</code> will lead to 
102   *        a <code>default N'SANDNES'</code> in DDL. Note that this is platform specific!</li>
103   * </ul>
104   */
105  public static String toSqlLiteral(String defaultValue, Class<?> propertyType, int sqlType) {
106    if (propertyType == null
107        || defaultValue == null 
108        || NULL.equals(defaultValue)
109        || (defaultValue.startsWith("'") && defaultValue.endsWith("'"))
110        || (defaultValue.startsWith("$RAW:"))) {  
111      return defaultValue;
112    }
113
114    if (Boolean.class.isAssignableFrom(propertyType) || Boolean.TYPE.isAssignableFrom(propertyType)) {
115      return toBooleanLiteral(defaultValue);
116    }
117    
118    if (Number.class.isAssignableFrom(propertyType) 
119        || Byte.TYPE.equals(propertyType)
120        || Short.TYPE.equals(propertyType)
121        || Integer.TYPE.equals(propertyType)
122        || Long.TYPE.equals(propertyType)
123        || Float.TYPE.equals(propertyType)
124        || Double.TYPE.equals(propertyType)
125        || (propertyType.isEnum() && sqlType == Types.INTEGER)) {
126      Double.valueOf(defaultValue); // verify if it is a number
127      return defaultValue;
128    }
129   
130    // check if it is a date/time - in all other cases return quoted defaultValue
131    switch (sqlType) {
132      // date
133      case Types.DATE:
134        return toDateLiteral(defaultValue);
135      // time
136      case Types.TIME:
137      case Types.TIME_WITH_TIMEZONE:
138        return toTimeLiteral(defaultValue);
139      // timestamp
140      case Types.TIMESTAMP:
141      case Types.TIMESTAMP_WITH_TIMEZONE:
142        return toDateTimeLiteral(defaultValue);
143
144      default:
145        return toTextLiteral(defaultValue); // do not check other datatypes
146    }
147  }
148
149  /**
150   * Checks if specified value is either 'true' or 'false'. The literal is translated later.
151   */
152  private static String toBooleanLiteral(String value) {
153    if (DbDefaultValue.FALSE.equals(value) || DbDefaultValue.TRUE.equals(value)) {
154      return value;
155    }
156    throw new IllegalArgumentException("'" + value + "' is not a valid value for boolean");
157  }
158  
159  /**
160   * This adds single qoutes around the <code>value</code> and doubles single quotes.
161   * "User's home" will return "'User''s home'"
162   */
163  private static String toTextLiteral(String value) {
164    StringBuilder sb = new StringBuilder(value.length()+10);
165    sb.append('\'');
166    for (int i = 0; i < value.length(); i++) {
167      char ch = value.charAt(i);
168      if (ch == '\'') {
169        sb.append("''");
170      } else {
171        sb.append(ch);
172      }
173    }
174    sb.append('\'');
175    return sb.toString();
176
177  }
178  
179  private static String toDateLiteral(String value) {
180    if (NOW.equals(value)) {
181      return value; // this will get translated later
182    }
183    DatatypeConverter.parseDate(value); // verify
184    return toTextLiteral(value);
185  }
186
187  private static String toTimeLiteral(String value) {
188    if (NOW.equals(value)) {
189      return value; // this will get translated later
190    }
191    DatatypeConverter.parseTime(value); // verify
192    return toTextLiteral(value);
193  }
194  
195  private static String toDateTimeLiteral(String value) {
196    if (NOW.equals(value)) {
197      return value; // this will get translated later
198    }
199    DatatypeConverter.parseDateTime(value); // verify
200    return toTextLiteral(value);
201  }
202}