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