001package gudusoft.gsqlparser.sqlenv; 002 003import gudusoft.gsqlparser.nodes.TTypeName; 004import gudusoft.gsqlparser.util.SQLUtil; 005 006import java.util.*; 007 008public abstract class TSQLRoutine extends TSQLSchemaObject{ 009 // volatile: written during registration, read lock-free by other threads 010 private volatile String definition; 011 012 public void setDefinition(String definition) { 013 this.definition = definition; 014 } 015 016 public String getDefinition() { 017 return definition; 018 } 019 020 public TSQLRoutine(TSQLSchema schema, String objectName, ESQLDataObjectType dataObjectType){ 021 super(schema,objectName,dataObjectType); 022 } 023 024 /** 025 * column list 026 * @return a column list 027 */ 028 public List<TSQLParameter> getParameterList() { 029 List<TSQLParameter> parameterList = new LinkedList<>(); 030 Map<String, TSQLParameter> m = getParameterMap(); 031 synchronized (m) { 032 for(String parameter: m.keySet()){ 033 parameterList.add(m.get(parameter)); 034 } 035 } 036 return parameterList; 037 } 038 039 040 // volatile + double-checked lazy init: concurrent analyzers registering the 041 // same routine share this instance, so the map must be published safely 042 private volatile Map<String, TSQLParameter> parameterMap; 043 044 private Map<String, TSQLParameter> getParameterMap() { 045 Map<String, TSQLParameter> m = parameterMap; 046 if (m == null) { 047 synchronized (this) { 048 if (parameterMap == null) { 049 parameterMap = Collections.synchronizedMap(new LinkedHashMap<String, TSQLParameter>()); 050 } 051 m = parameterMap; 052 } 053 } 054 return m; 055 } 056 057 058 public void addParameter(int index, TTypeName parameterDataType){ 059 Map<String, TSQLParameter> m = getParameterMap(); 060 // search-then-put must be atomic under concurrent registration 061 synchronized (m) { 062 if (!searchParameter(index,parameterDataType,null)){ 063 m.put(SQLUtil.getIdentifierNormalColumnName(this.sqlEnv.getDBVendor(), makeParameterMapId(index,parameterDataType,null)), new TSQLParameter(this, index,parameterDataType,null)); 064 } 065 } 066 } 067 068 public void addParameter(int index, TTypeName parameterDataType,String parameterName){ 069 Map<String, TSQLParameter> m = getParameterMap(); 070 // search-then-put must be atomic under concurrent registration 071 synchronized (m) { 072 if (!searchParameter(index,parameterDataType,parameterName)){ 073 m.put(SQLUtil.getIdentifierNormalColumnName(this.sqlEnv.getDBVendor(), makeParameterMapId(index,parameterDataType,parameterName)), new TSQLParameter(this,index,parameterDataType,parameterName)); 074 } 075 } 076 } 077 078 079 public boolean searchParameter(int index, TTypeName datatype, String parameterName){ 080 return getParameterMap().containsKey(SQLUtil.getIdentifierNormalColumnName(this.sqlEnv.getDBVendor() 081 , makeParameterMapId(index,datatype,parameterName))); 082 } 083 084 public TSQLParameter getParameter(int index, TTypeName datatype, String parameterName){ 085 return getParameterMap().get(SQLUtil.getIdentifierNormalColumnName(this.sqlEnv.getDBVendor() 086 , makeParameterMapId(index,datatype,parameterName))); 087 } 088 089 String makeParameterMapId(int index, TTypeName datatype, String parameterName){ 090 return index+datatype.toString()+parameterName == null?"N/A":parameterName; 091 } 092 093 /** 094 * A value set in advance for a parameter of the routine (Mantis 4962): the dynamic SQL evaluation of the routine 095 * reads it as the parameter's value, ahead of the routine's call sites, as a caller passing it would. A 096 * {@link Kind#SUPPLIED_UNKNOWN} value says a caller passes a value nobody knows: the parameter is unknown, not 097 * absent, so its default does not apply. 098 */ 099 public static final class PresetValue { 100 public enum Kind { STRING, NUMBER, SQL_NULL, SUPPLIED_UNKNOWN } 101 102 private final Kind kind; 103 private final String text; 104 105 private PresetValue(Kind kind, String text) { 106 this.kind = kind; 107 this.text = text; 108 } 109 110 public static PresetValue string(String value) { 111 return value == null ? sqlNull() : new PresetValue(Kind.STRING, value); 112 } 113 114 /** A number, as written ({@code "5"}, {@code "1.5"}). */ 115 public static PresetValue number(String text) { 116 if (text == null) { 117 return sqlNull(); 118 } 119 new java.math.BigDecimal(text.trim()); // a number or an IllegalArgumentException 120 return new PresetValue(Kind.NUMBER, text.trim()); 121 } 122 123 public static PresetValue sqlNull() { 124 return new PresetValue(Kind.SQL_NULL, null); 125 } 126 127 public static PresetValue suppliedUnknown() { 128 return new PresetValue(Kind.SUPPLIED_UNKNOWN, null); 129 } 130 131 public Kind getKind() { 132 return kind; 133 } 134 135 /** The string or the number as written; null for SQL NULL and for a supplied unknown value. */ 136 public String getText() { 137 return text; 138 } 139 140 @Override 141 public String toString() { 142 switch (kind) { 143 case STRING: 144 return "'" + text + "'"; 145 case NUMBER: 146 return text; 147 case SQL_NULL: 148 return "NULL"; 149 default: 150 return "(supplied, not known)"; 151 } 152 } 153 } 154 155 /** The presets by the parameter's identity, each with the name it was set under; built on first use. */ 156 private volatile Map<CanonKey, Map.Entry<String, PresetValue>> parameterPresets; 157 158 private Map<CanonKey, Map.Entry<String, PresetValue>> presets() { 159 Map<CanonKey, Map.Entry<String, PresetValue>> m = parameterPresets; 160 if (m == null) { 161 synchronized (this) { 162 if (parameterPresets == null) { 163 parameterPresets = Collections.synchronizedMap( 164 new LinkedHashMap<CanonKey, Map.Entry<String, PresetValue>>()); 165 } 166 m = parameterPresets; 167 } 168 } 169 return m; 170 } 171 172 /** The parameter's identity under the catalog's own identifier rules (its configured profile). */ 173 private CanonKey presetKey(String parameterName) { 174 return this.sqlEnv.getIdentifierService().canonKey(parameterName, ESQLDataObjectType.dotColumn); 175 } 176 177 /** 178 * Sets, or with a null value removes, the preset of the parameter named {@code parameterName}, as the procedure 179 * declares it ({@code @pipeline_type}); the name is compared as the vendor compares column names. 180 */ 181 public void setParameterPreset(String parameterName, PresetValue value) { 182 if (parameterName == null) { 183 return; 184 } 185 if (value == null) { 186 if (parameterPresets != null) { 187 parameterPresets.remove(presetKey(parameterName)); 188 } 189 } else { 190 presets().put(presetKey(parameterName), 191 new AbstractMap.SimpleImmutableEntry<String, PresetValue>(parameterName, value)); 192 // the catalog finds a routine with presets without a lookup that could fetch metadata 193 this.sqlEnv.registerPresetRoutine(this); 194 } 195 } 196 197 /** The preset of the parameter named {@code parameterName}, or null when none is set. */ 198 public PresetValue getParameterPreset(String parameterName) { 199 Map<CanonKey, Map.Entry<String, PresetValue>> m = parameterPresets; 200 Map.Entry<String, PresetValue> entry = parameterName == null || m == null ? null 201 : m.get(presetKey(parameterName)); 202 return entry == null ? null : entry.getValue(); 203 } 204 205 /** Every preset, by the name it was set under, in the order set. */ 206 public Map<String, PresetValue> getParameterPresets() { 207 Map<String, PresetValue> presets = new LinkedHashMap<String, PresetValue>(); 208 Map<CanonKey, Map.Entry<String, PresetValue>> m = parameterPresets; 209 if (m == null) { 210 return presets; 211 } 212 synchronized (m) { 213 for (Map.Entry<String, PresetValue> entry : m.values()) { 214 presets.put(entry.getKey(), entry.getValue()); 215 } 216 } 217 return presets; 218 } 219 220 private static final java.util.logging.Logger LOG = java.util.logging.Logger.getLogger(TSQLRoutine.class.getName()); 221 222 /** 223 * Sets the presets a metadata file gives the routine ({@code procedures[].parameterBindings}): each entry a 224 * parameter name and a JSON value, a string, a number, or null for SQL NULL. A value of another kind (true, 225 * false, an object, an array) is not read and is logged with {@code origin}. A later file's preset of a parameter 226 * replaces an earlier one's. 227 */ 228 public void applyParameterBindings(Map<?, ?> bindings, String origin) { 229 if (bindings == null) { 230 return; 231 } 232 for (Map.Entry<?, ?> entry : bindings.entrySet()) { 233 if (!(entry.getKey() instanceof String)) { 234 continue; 235 } 236 String parameter = (String) entry.getKey(); 237 Object value = entry.getValue(); 238 PresetValue preset; 239 try { 240 if (value == null) { 241 preset = PresetValue.sqlNull(); 242 } else if (value instanceof String) { 243 preset = PresetValue.string((String) value); 244 } else if (value instanceof Number) { 245 preset = PresetValue.number(numberText((Number) value)); 246 } else { 247 LOG.warning("parameterBindings of " + getName() + (origin == null ? "" : " in " + origin) + ": " 248 + parameter + " has a value that is not a string, a number or null; it is not read"); 249 continue; 250 } 251 } catch (RuntimeException e) { 252 // one value the catalog cannot hold (a number out of range): that binding only is skipped 253 LOG.warning("parameterBindings of " + getName() + (origin == null ? "" : " in " + origin) + ": " 254 + parameter + " = " + value + " is not read: " + e.getMessage()); 255 continue; 256 } 257 replacePreset(parameter, preset, origin); 258 } 259 } 260 261 /** Takes over {@code from}'s presets; one it gives for a parameter replaces this routine's, logged. */ 262 public void mergeParameterPresets(TSQLRoutine from, String origin) { 263 if (from == null || from == this) { 264 return; 265 } 266 for (Map.Entry<String, PresetValue> preset : from.getParameterPresets().entrySet()) { 267 replacePreset(preset.getKey(), preset.getValue(), origin); 268 } 269 } 270 271 private void replacePreset(String parameter, PresetValue preset, String origin) { 272 PresetValue earlier = getParameterPreset(parameter); 273 if (earlier != null && (earlier.getKind() != preset.getKind() 274 || !String.valueOf(earlier.getText()).equals(String.valueOf(preset.getText())))) { 275 LOG.info("preset of " + parameter + " of " + getName() + (origin == null ? "" : " from " + origin) + ": " 276 + preset + " replaces " + earlier); 277 } 278 setParameterPreset(parameter, preset); 279 } 280 281 /** 282 * A JSON number's text: exact for the integers and long decimals the parser keeps exact ({@code BigInteger}, 283 * {@code BigDecimal}, {@code Long}); for a shorter decimal the parser reads as a double, the shortest text of 284 * that double ({@code 5.0} stays {@code 5.0}). A value no decimal holds (an infinite double) is an error. 285 */ 286 private static String numberText(Number n) { 287 if (n instanceof java.math.BigDecimal) { 288 return ((java.math.BigDecimal) n).toPlainString(); 289 } 290 if (n instanceof Double || n instanceof Float) { 291 return java.math.BigDecimal.valueOf(n.doubleValue()).toPlainString(); 292 } 293 return n.toString(); 294 } 295 296 297}