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}