OpenConcerto

Dépôt officiel du code source de l'ERP OpenConcerto
sonarqube

svn://code.openconcerto.org/openconcerto

Rev

Rev 156 | Only display areas with differences | Regard whitespace | Details | Blame | Last modification | View Log | RSS feed

Rev 156 Rev 182
1
/*
1
/*
2
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
2
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
3
 * 
3
 * 
4
 * Copyright 2011 OpenConcerto, by ILM Informatique. All rights reserved.
4
 * Copyright 2011-2019 OpenConcerto, by ILM Informatique. All rights reserved.
5
 * 
5
 * 
6
 * The contents of this file are subject to the terms of the GNU General Public License Version 3
6
 * The contents of this file are subject to the terms of the GNU General Public License Version 3
7
 * only ("GPL"). You may not use this file except in compliance with the License. You can obtain a
7
 * only ("GPL"). You may not use this file except in compliance with the License. You can obtain a
8
 * copy of the License at http://www.gnu.org/licenses/gpl-3.0.html See the License for the specific
8
 * copy of the License at http://www.gnu.org/licenses/gpl-3.0.html See the License for the specific
9
 * language governing permissions and limitations under the License.
9
 * language governing permissions and limitations under the License.
10
 * 
10
 * 
11
 * When distributing the software, include this License Header Notice in each file.
11
 * When distributing the software, include this License Header Notice in each file.
12
 */
12
 */
13
 
13
 
14
 /*
14
 /*
15
 * Field created on 4 mai 2004
15
 * Field created on 4 mai 2004
16
 */
16
 */
17
package org.openconcerto.sql.model;
17
package org.openconcerto.sql.model;
18
 
18
 
19
import static org.openconcerto.sql.model.SQLBase.quoteIdentifier;
19
import static org.openconcerto.sql.model.SQLBase.quoteIdentifier;
20
 
20
 
21
import org.openconcerto.sql.Log;
21
import org.openconcerto.sql.Log;
22
import org.openconcerto.sql.model.SQLTable.FieldGroup;
22
import org.openconcerto.sql.model.SQLTable.FieldGroup;
23
import org.openconcerto.sql.model.graph.Link;
23
import org.openconcerto.sql.model.graph.Link;
24
import org.openconcerto.sql.model.graph.Path;
24
import org.openconcerto.sql.model.graph.Path;
25
import org.openconcerto.sql.model.graph.SQLKey.Type;
25
import org.openconcerto.sql.model.graph.SQLKey.Type;
26
import org.openconcerto.utils.CollectionUtils;
26
import org.openconcerto.utils.CollectionUtils;
27
import org.openconcerto.utils.CompareUtils;
27
import org.openconcerto.utils.CompareUtils;
28
import org.openconcerto.utils.ExceptionUtils;
28
import org.openconcerto.utils.ExceptionUtils;
29
import org.openconcerto.utils.Value;
29
import org.openconcerto.utils.Value;
30
import org.openconcerto.xml.JDOM2Utils;
30
import org.openconcerto.xml.JDOM2Utils;
31
import org.openconcerto.xml.XMLCodecUtils;
31
import org.openconcerto.xml.XMLCodecUtils;
32
 
32
 
33
import java.math.BigDecimal;
33
import java.math.BigDecimal;
34
import java.sql.DatabaseMetaData;
34
import java.sql.DatabaseMetaData;
35
import java.sql.ResultSet;
35
import java.sql.ResultSet;
36
import java.sql.SQLException;
36
import java.sql.SQLException;
37
import java.sql.Time;
37
import java.sql.Time;
38
import java.sql.Timestamp;
38
import java.sql.Timestamp;
39
import java.util.Collections;
39
import java.util.Collections;
40
import java.util.Date;
40
import java.util.Date;
41
import java.util.HashMap;
41
import java.util.HashMap;
42
import java.util.Iterator;
42
import java.util.Iterator;
43
import java.util.Map;
43
import java.util.Map;
44
import java.util.Map.Entry;
44
import java.util.Map.Entry;
45
import java.util.logging.Level;
45
import java.util.logging.Level;
46
import java.util.regex.Matcher;
46
import java.util.regex.Matcher;
47
import java.util.regex.Pattern;
47
import java.util.regex.Pattern;
48
 
48
 
49
import org.jdom2.Element;
49
import org.jdom2.Element;
50
 
50
 
51
import net.jcip.annotations.GuardedBy;
51
import net.jcip.annotations.GuardedBy;
52
import net.jcip.annotations.ThreadSafe;
52
import net.jcip.annotations.ThreadSafe;
53
 
53
 
54
/**
54
/**
55
 * Un champ SQL. Pour obtenir une instance de cette classe il faut utiliser
55
 * Un champ SQL. Pour obtenir une instance de cette classe il faut utiliser
56
 * {@link SQLTable#getField(String)}. Un champ connait sa table, son nom, son type et sa valeur par
56
 * {@link SQLTable#getField(String)}. Un champ connait sa table, son nom, son type et sa valeur par
57
 * défaut.
57
 * défaut.
58
 * 
58
 * 
59
 * @author ILM Informatique 4 mai 2004
59
 * @author ILM Informatique 4 mai 2004
60
 */
60
 */
61
@ThreadSafe
61
@ThreadSafe
62
public class SQLField extends SQLIdentifier implements FieldRef, IFieldPath {
62
public class SQLField extends SQLIdentifier implements FieldRef, IFieldPath {
63
 
63
 
64
    static final char CHAR = '|';
64
    static final char CHAR = '|';
65
 
65
 
66
    // nextVal('"SCHEMA"."seqName"'::regclass);
66
    // nextVal('"SCHEMA"."seqName"'::regclass);
67
    static private final Pattern SEQ_PATTERN = Pattern.compile("nextval\\('(.+)'.*\\)");
67
    static private final Pattern SEQ_PATTERN = Pattern.compile("nextval\\('(.+)'.*\\)");
68
 
68
 
69
    static final SQLField create(SQLTable t, ResultSet rs) throws SQLException {
69
    static final SQLField create(SQLTable t, ResultSet rs) throws SQLException {
70
        final SQLSystem system = t.getServer().getSQLSystem();
70
        final SQLSystem system = t.getServer().getSQLSystem();
71
        final String fieldName = rs.getString("COLUMN_NAME");
71
        final String fieldName = rs.getString("COLUMN_NAME");
72
 
72
 
73
        final int dataType = rs.getInt("DATA_TYPE");
73
        final int dataType = rs.getInt("DATA_TYPE");
74
        final int size = rs.getInt("COLUMN_SIZE");
74
        final int size = rs.getInt("COLUMN_SIZE");
75
        final SQLType type;
75
        final SQLType type;
76
 
76
 
77
        try {
77
        try {
78
            // MS doesn't return an int
78
            // MS doesn't return an int
79
            final Object decDig = rs.getObject("DECIMAL_DIGITS");
79
            final Object decDig = rs.getObject("DECIMAL_DIGITS");
80
            final Integer intDecDig = (Integer) (decDig == null || decDig instanceof Integer ? decDig : ((Number) decDig).intValue());
80
            final Integer intDecDig = (Integer) (decDig == null || decDig instanceof Integer ? decDig : ((Number) decDig).intValue());
81
            String typeName = rs.getString("TYPE_NAME");
81
            String typeName = rs.getString("TYPE_NAME");
82
            if (system == SQLSystem.POSTGRESQL) {
82
            if (system == SQLSystem.POSTGRESQL) {
83
                // AbstractJdbc2DatabaseMetaData.getColumns() convert the true type to serial. But
83
                // AbstractJdbc2DatabaseMetaData.getColumns() convert the true type to serial. But
84
                // the default value is kept, which is redundant. Further in the framework we need
84
                // the default value is kept, which is redundant. Further in the framework we need
85
                // to know the true type (e.g. to cast), and there's SQLSyntax.isAuto()/getAuto() to
85
                // to know the true type (e.g. to cast), and there's SQLSyntax.isAuto()/getAuto() to
86
                // handle serial.
86
                // handle serial.
87
                if (typeName.toLowerCase().equals("bigserial"))
87
                if (typeName.toLowerCase().equals("bigserial"))
88
                    typeName = "int8";
88
                    typeName = "int8";
89
                else if (typeName.toLowerCase().equals("serial"))
89
                else if (typeName.toLowerCase().equals("serial"))
90
                    typeName = "int4";
90
                    typeName = "int4";
91
            }
91
            }
92
            type = SQLType.get(t.getBase(), dataType, size, intDecDig, typeName);
92
            type = SQLType.get(t.getBase(), dataType, size, intDecDig, typeName);
93
        } catch (IllegalStateException e) {
93
        } catch (IllegalStateException e) {
94
            throw ExceptionUtils.createExn(IllegalStateException.class, "can't create " + t + " " + fieldName, e);
94
            throw ExceptionUtils.createExn(IllegalStateException.class, "can't create " + t + " " + fieldName, e);
95
        }
95
        }
96
 
96
 
97
        final Map<String, Object> map;
97
        final Map<String, Object> map;
98
        // MS sql throws an exception for rs.getObject("IS_AUTOINCREMENT") :
98
        // MS sql throws an exception for rs.getObject("IS_AUTOINCREMENT") :
99
        // La conversion de char en SMALLINT n'est pas prise en charge.
99
        // La conversion de char en SMALLINT n'est pas prise en charge.
100
        if (system == SQLSystem.MSSQL) {
100
        if (system == SQLSystem.MSSQL) {
101
            map = SQLDataSource.ROW_PROC.toMap(rs, Collections.singleton("IS_AUTOINCREMENT"));
101
            map = SQLDataSource.ROW_PROC.toMap(rs, Collections.singleton("IS_AUTOINCREMENT"));
102
            // get*(String) is costly so only use it for MS
102
            // get*(String) is costly so only use it for MS
103
            map.put("IS_AUTOINCREMENT", rs.getString("IS_AUTOINCREMENT"));
103
            map.put("IS_AUTOINCREMENT", rs.getString("IS_AUTOINCREMENT"));
104
        } else {
104
        } else {
105
            map = SQLDataSource.ROW_PROC.toMap(rs);
105
            map = SQLDataSource.ROW_PROC.toMap(rs);
106
        }
106
        }
107
 
107
 
108
        return new SQLField(t, fieldName, type, map);
108
        return new SQLField(t, fieldName, type, map);
109
    }
109
    }
110
 
110
 
111
    static private Boolean nullableStr2Obj(final String isNullable) {
111
    static private Boolean nullableStr2Obj(final String isNullable) {
112
        final Boolean res;
112
        final Boolean res;
113
        if ("YES".equalsIgnoreCase(isNullable))
113
        if ("YES".equalsIgnoreCase(isNullable))
114
            res = Boolean.TRUE;
114
            res = Boolean.TRUE;
115
        else if ("NO".equalsIgnoreCase(isNullable))
115
        else if ("NO".equalsIgnoreCase(isNullable))
116
            res = Boolean.FALSE;
116
            res = Boolean.FALSE;
117
        else
117
        else
118
            res = null;
118
            res = null;
119
        return res;
119
        return res;
120
    }
120
    }
121
 
121
 
122
    @SuppressWarnings("unchecked")
122
    @SuppressWarnings("unchecked")
123
    static SQLField create(SQLTable t, Element elementField) {
123
    static SQLField create(SQLTable t, Element elementField) {
124
        final String fieldName = elementField.getAttributeValue("name");
124
        final String fieldName = elementField.getAttributeValue("name");
125
 
125
 
126
        SQLType type = SQLType.get(t.getBase(), elementField.getChild("type"));
126
        SQLType type = SQLType.get(t.getBase(), elementField.getChild("type"));
127
        final Map<String, Object> metadata = (Map<String, Object>) XMLCodecUtils.decode1(elementField.getChild("java"));
127
        final Map<String, Object> metadata = (Map<String, Object>) XMLCodecUtils.decode1(elementField.getChild("java"));
128
        final Map<String, Object> infoSchema = (Map<String, Object>) XMLCodecUtils.decode1(elementField.getChild("infoSchema").getChild("java"));
128
        final Map<String, Object> infoSchema = (Map<String, Object>) XMLCodecUtils.decode1(elementField.getChild("infoSchema").getChild("java"));
129
 
129
 
130
        final SQLField res = new SQLField(t, fieldName, type, metadata);
130
        final SQLField res = new SQLField(t, fieldName, type, metadata);
131
        res.setColsFromInfoSchema(infoSchema);
131
        res.setColsFromInfoSchema(infoSchema);
132
        return res;
132
        return res;
133
    }
133
    }
134
 
134
 
135
    /**
135
    /**
136
     * Properties of a field.
136
     * Properties of a field.
137
     * 
137
     * 
138
     * @author Sylvain
138
     * @author Sylvain
139
     */
139
     */
140
    public static enum Properties {
140
    public static enum Properties {
141
        NAME, TYPE, DEFAULT, NULLABLE
141
        NAME, TYPE, DEFAULT, NULLABLE
142
    };
142
    };
143
 
143
 
144
    private final String fullName;
144
    private final String fullName;
145
 
145
 
146
    // all following attributes guarded by "this"
146
    // all following attributes guarded by "this"
147
    private SQLType type;
147
    private SQLType type;
148
    private final Map<String, Object> metadata;
148
    private final Map<String, Object> metadata;
149
    private String defaultValue;
149
    private String defaultValue;
150
    @GuardedBy("this")
150
    @GuardedBy("this")
151
    private Value<Object> parsedDefaultValue;
151
    private Value<Object> parsedDefaultValue;
152
    private Boolean nullable;
152
    private Boolean nullable;
153
    // from information_schema.COLUMNS
153
    // from information_schema.COLUMNS
154
    private final Map<String, Object> infoSchemaCols;
154
    private final Map<String, Object> infoSchemaCols;
155
 
155
 
156
    private String xml;
156
    private String xml;
157
 
157
 
158
    SQLField(SQLTable table, String name, SQLType type, Map<String, Object> metadata) {
158
    SQLField(SQLTable table, String name, SQLType type, Map<String, Object> metadata) {
159
        super(table, name);
159
        super(table, name);
160
        this.type = type;
160
        this.type = type;
161
        this.metadata = metadata;
161
        this.metadata = metadata;
162
        // quite a few entries have null values, remove them since we don't use keys
162
        // quite a few entries have null values, remove them since we don't use keys
163
        // and this take a decent amount of space when saved as XML
163
        // and this take a decent amount of space when saved as XML
164
        final Iterator<Entry<String, Object>> iter = this.metadata.entrySet().iterator();
164
        final Iterator<Entry<String, Object>> iter = this.metadata.entrySet().iterator();
165
        while (iter.hasNext()) {
165
        while (iter.hasNext()) {
166
            final Entry<String, Object> e = iter.next();
166
            final Entry<String, Object> e = iter.next();
167
            if (e.getValue() == null)
167
            if (e.getValue() == null)
168
                iter.remove();
168
                iter.remove();
169
        }
169
        }
170
        // pg jdbc use pg_catalog.pg_attrdef.adsrc (see
170
        // pg jdbc use pg_catalog.pg_attrdef.adsrc (see
171
        // org.postgresql.jdbc2.AbstractJdbc2DatabaseMetaData#getColumns()) but should use
171
        // org.postgresql.jdbc2.AbstractJdbc2DatabaseMetaData#getColumns()) but should use
172
        // pg_get_expr(adbin) (see 44.6. pg_attrdef), this sometimes result in
172
        // pg_get_expr(adbin) (see 44.6. pg_attrdef), this sometimes result in
173
        // <nextval('"Preventec_Common"."DISCIPLINE_ID_seq"'::regclass)> !=
173
        // <nextval('"Preventec_Common"."DISCIPLINE_ID_seq"'::regclass)> !=
174
        // <nextval('"DISCIPLINE_ID_seq"'::regclass)>
174
        // <nextval('"DISCIPLINE_ID_seq"'::regclass)>
175
        this.defaultValue = (String) metadata.get("COLUMN_DEF");
175
        this.defaultValue = (String) metadata.get("COLUMN_DEF");
176
        // don't parse now, as it might not be possible : i.e. function calls
176
        // don't parse now, as it might not be possible : i.e. function calls
177
        this.parsedDefaultValue = null;
177
        this.parsedDefaultValue = null;
178
        this.fullName = this.getTable().getName() + "." + this.getName();
178
        this.fullName = this.getTable().getName() + "." + this.getName();
179
        this.nullable = nullableStr2Obj((String) metadata.get("IS_NULLABLE"));
179
        this.nullable = nullableStr2Obj((String) metadata.get("IS_NULLABLE"));
180
 
180
 
181
        this.infoSchemaCols = new HashMap<String, Object>();
181
        this.infoSchemaCols = new HashMap<String, Object>();
182
 
182
 
183
        this.xml = null;
183
        this.xml = null;
184
    }
184
    }
185
 
185
 
186
    SQLField(SQLTable table, SQLField f) {
186
    SQLField(SQLTable table, SQLField f) {
187
        super(table, f.getName());
187
        super(table, f.getName());
188
        this.type = f.type;
188
        this.type = f.type;
189
        this.metadata = new HashMap<String, Object>(f.metadata);
189
        this.metadata = new HashMap<String, Object>(f.metadata);
190
        this.defaultValue = f.defaultValue;
190
        this.defaultValue = f.defaultValue;
191
        this.parsedDefaultValue = f.parsedDefaultValue;
191
        this.parsedDefaultValue = f.parsedDefaultValue;
192
        this.fullName = f.fullName;
192
        this.fullName = f.fullName;
193
        this.nullable = f.nullable;
193
        this.nullable = f.nullable;
194
        this.infoSchemaCols = new HashMap<String, Object>(f.infoSchemaCols);
194
        this.infoSchemaCols = new HashMap<String, Object>(f.infoSchemaCols);
195
        this.xml = f.xml;
195
        this.xml = f.xml;
196
    }
196
    }
197
 
197
 
198
    synchronized void mutateTo(SQLField f) {
198
    synchronized void mutateTo(SQLField f) {
199
        if (this == f)
199
        if (this == f)
200
            return;
200
            return;
201
        this.type = f.type;
201
        this.type = f.type;
202
        this.metadata.clear();
202
        this.metadata.clear();
203
        this.metadata.putAll(f.metadata);
203
        this.metadata.putAll(f.metadata);
204
        this.defaultValue = f.defaultValue;
204
        this.defaultValue = f.defaultValue;
205
        this.parsedDefaultValue = f.parsedDefaultValue;
205
        this.parsedDefaultValue = f.parsedDefaultValue;
206
        this.nullable = f.nullable;
206
        this.nullable = f.nullable;
207
        this.setColsFromInfoSchema(f.infoSchemaCols);
207
        this.setColsFromInfoSchema(f.infoSchemaCols);
208
        this.xml = f.xml;
208
        this.xml = f.xml;
209
    }
209
    }
210
 
210
 
211
    @SuppressWarnings("unchecked")
211
    @SuppressWarnings("unchecked")
212
    synchronized void setColsFromInfoSchema(Map m) {
212
    synchronized void setColsFromInfoSchema(Map m) {
213
        this.infoSchemaCols.clear();
213
        this.infoSchemaCols.clear();
214
        this.infoSchemaCols.putAll(m);
214
        this.infoSchemaCols.putAll(m);
215
        this.infoSchemaCols.keySet().removeAll(SQLSyntax.INFO_SCHEMA_NAMES_KEYS);
215
        this.infoSchemaCols.keySet().removeAll(SQLSyntax.INFO_SCHEMA_NAMES_KEYS);
216
    }
216
    }
217
 
217
 
218
    @Override
218
    @Override
219
    public String toString() {
219
    public String toString() {
220
        return CHAR + this.getFullName() + CHAR;
220
        return CHAR + this.getFullName() + CHAR;
221
    }
221
    }
222
 
222
 
223
    /**
223
    /**
224
     * Le nom complet de ce champ.
224
     * Le nom complet de ce champ.
225
     * 
225
     * 
226
     * @return le nom complet de ce champ, ie NOM_TABLE.NOM_CHAMP.
226
     * @return le nom complet de ce champ, ie NOM_TABLE.NOM_CHAMP.
227
     */
227
     */
228
    public synchronized final String getFullName() {
228
    public synchronized final String getFullName() {
229
        return this.fullName;
229
        return this.fullName;
230
    }
230
    }
231
 
231
 
232
    public SQLTable getTable() {
232
    public SQLTable getTable() {
233
        return (SQLTable) this.getParent();
233
        return (SQLTable) this.getParent();
234
    }
234
    }
235
 
235
 
236
    public synchronized SQLType getType() {
236
    public synchronized SQLType getType() {
237
        return this.type;
237
        return this.type;
238
    }
238
    }
239
 
239
 
240
    /**
240
    /**
241
     * Return the type of this field in SQL.
241
     * Return the type of this field in SQL.
242
     * 
242
     * 
243
     * @return the SQL for the type, e.g. "int" or "decimal(16,8)".
243
     * @return the SQL for the type, e.g. "int" or "decimal(16,8)".
244
     * @see SQLSyntax#getType(SQLField)
244
     * @see SQLSyntax#getType(SQLField)
245
     */
245
     */
246
    public final String getTypeDecl() {
246
    public final String getTypeDecl() {
247
        return this.getDBSystemRoot().getSyntax().getType(this);
247
        return this.getDBSystemRoot().getSyntax().getType(this);
248
    }
248
    }
249
 
249
 
250
    /**
250
    /**
251
     * Metadata from JDBC.
251
     * Metadata from JDBC.
252
     * 
252
     * 
253
     * @param name metadata name, eg "DECIMAL_DIGITS".
253
     * @param name metadata name, eg "DECIMAL_DIGITS".
254
     * @return the value.
254
     * @return the value.
255
     * @see DatabaseMetaData#getColumns(String, String, String, String)
255
     * @see DatabaseMetaData#getColumns(String, String, String, String)
256
     */
256
     */
257
    public synchronized Object getMetadata(String name) {
257
    public synchronized Object getMetadata(String name) {
258
        return this.metadata.get(name);
258
        return this.metadata.get(name);
259
    }
259
    }
260
 
260
 
261
    /**
261
    /**
262
     * Additional metadata from INFORMATION_SCHEMA.
262
     * Additional metadata from INFORMATION_SCHEMA.
263
     * 
263
     * 
264
     * @return metadata from INFORMATION_SCHEMA.
264
     * @return metadata from INFORMATION_SCHEMA.
265
     * @see #getMetadata(String)
265
     * @see #getMetadata(String)
266
     */
266
     */
267
    public synchronized final Map<String, Object> getInfoSchema() {
267
    public synchronized final Map<String, Object> getInfoSchema() {
268
        return Collections.unmodifiableMap(this.infoSchemaCols);
268
        return Collections.unmodifiableMap(this.infoSchemaCols);
269
    }
269
    }
270
 
270
 
271
    /**
271
    /**
272
     * The sequence linked to this field. I.e. that sequence will be dropped if this field is.
272
     * The sequence linked to this field. I.e. that sequence will be dropped if this field is.
273
     * 
273
     * 
274
     * @return the quoted name of the sequence, <code>null</code> if none.
274
     * @return the quoted name of the sequence, <code>null</code> if none.
275
     */
275
     */
276
    public final SQLName getOwnedSequence() {
276
    public final SQLName getOwnedSequence() {
277
        return this.getOwnedSequence(false);
277
        return this.getOwnedSequence(false);
278
    }
278
    }
279
 
279
 
280
    public final SQLName getOwnedSequence(final boolean allowRequest) {
280
    public final SQLName getOwnedSequence(final boolean allowRequest) {
281
        final SQLSystem sys = getServer().getSQLSystem();
281
        final SQLSystem sys = getServer().getSQLSystem();
282
        if (sys == SQLSystem.H2) {
282
        if (sys == SQLSystem.H2) {
283
            final String name = (String) this.infoSchemaCols.get("SEQUENCE_NAME");
283
            final String name = (String) this.infoSchemaCols.get("SEQUENCE_NAME");
284
            if (name != null) {
284
            if (name != null) {
285
                // H2 doesn't provide the schema name, but requires it when altering a field
285
                // H2 doesn't provide the schema name, but requires it when altering a field
286
                return new SQLName(getDBRoot().getName(), name);
286
                return new SQLName(getDBRoot().getName(), name);
287
            }
287
            }
288
        } else if (sys == SQLSystem.POSTGRESQL) {
288
        } else if (sys == SQLSystem.POSTGRESQL) {
289
            if (allowRequest) {
289
            if (allowRequest) {
290
                final String req = "SELECT pg_get_serial_sequence(" + getTable().getBase().quoteString(getTable().getSQLName().quote()) + ", " + getTable().getBase().quoteString(this.getName()) + ")";
290
                final String req = "SELECT pg_get_serial_sequence(" + getTable().getBase().quoteString(getTable().getSQLName().quote()) + ", " + getTable().getBase().quoteString(this.getName()) + ")";
291
                final String name = (String) getDBSystemRoot().getDataSource().executeScalar(req);
291
                final String name = (String) getDBSystemRoot().getDataSource().executeScalar(req);
292
                if (name != null)
292
                if (name != null)
293
                    return SQLName.parse(name);
293
                    return SQLName.parse(name);
294
            } else if (this.getDefaultValue() != null) {
294
            } else if (this.getDefaultValue() != null) {
295
                final String def = this.getDefaultValue().trim();
295
                final String def = this.getDefaultValue().trim();
296
                if (def.startsWith("nextval")) {
296
                if (def.startsWith("nextval")) {
297
                    final Matcher matcher = SEQ_PATTERN.matcher(def);
297
                    final Matcher matcher = SEQ_PATTERN.matcher(def);
298
                    if (matcher.matches()) {
298
                    if (matcher.matches()) {
299
                        return SQLName.parse(matcher.group(1));
299
                        return SQLName.parse(matcher.group(1));
300
                    } else {
300
                    } else {
301
                        throw new IllegalStateException("could not parse: " + def + " with " + SEQ_PATTERN.pattern());
301
                        throw new IllegalStateException("could not parse: " + def + " with " + SEQ_PATTERN.pattern());
302
                    }
302
                    }
303
                }
303
                }
304
            }
304
            }
305
        }
305
        }
306
        return null;
306
        return null;
307
    }
307
    }
308
 
308
 
309
    /**
309
    /**
310
     * The SQL default value.
310
     * The SQL default value.
311
     * 
311
     * 
312
     * @return the default value, e.g. <code>"1"</code> or <code>"'none'"</code>.
312
     * @return the default value, e.g. <code>"1"</code> or <code>"'none'"</code>.
313
     * @see DatabaseMetaData#getColumns(String, String, String, String)
313
     * @see DatabaseMetaData#getColumns(String, String, String, String)
314
     */
314
     */
315
    public synchronized String getDefaultValue() {
315
    public synchronized String getDefaultValue() {
316
        return this.defaultValue;
316
        return this.defaultValue;
317
    }
317
    }
318
 
318
 
319
    /**
319
    /**
320
     * Try to parse the SQL {@link #getDefaultValue() default value}. Numbers are always parsed to
320
     * Try to parse the SQL {@link #getDefaultValue() default value}. Numbers are always parsed to
321
     * {@link BigDecimal}.
321
     * {@link BigDecimal}.
322
     * 
322
     * 
323
     * @return {@link Value#getNone()} if parsing failed, otherwise the parsed value.
323
     * @return {@link Value#getNone()} if parsing failed, otherwise the parsed value.
324
     */
324
     */
325
    public synchronized Value<Object> getParsedDefaultValue() {
325
    public synchronized Value<Object> getParsedDefaultValue() {
326
        if (this.parsedDefaultValue == null) {
326
        if (this.parsedDefaultValue == null) {
327
            final Class<?> javaType = this.getType().getJavaType();
327
            final Class<?> javaType = this.getType().getJavaType();
328
            final String defaultVal = SQLSyntax.getNormalizedDefault(this);
328
            final String defaultVal = SQLSyntax.getNormalizedDefault(this);
329
            try {
329
            try {
330
                Object p = null;
330
                Object p = null;
331
                if (defaultVal == null || defaultVal.trim().equalsIgnoreCase("null")) {
331
                if (defaultVal == null || defaultVal.trim().equalsIgnoreCase("null")) {
332
                    p = null;
332
                    p = null;
333
                } else if (String.class.isAssignableFrom(javaType)) {
333
                } else if (String.class.isAssignableFrom(javaType)) {
334
                    // Strings can be encoded a lot of different ways, see SQLBase.quoteString()
334
                    // Strings can be encoded a lot of different ways, see SQLBase.quoteString()
335
                    if (defaultVal.charAt(0) == '\'' && defaultVal.indexOf('\\') == -1)
335
                    if (defaultVal.charAt(0) == '\'' && defaultVal.indexOf('\\') == -1)
336
                        p = SQLBase.unquoteStringStd(defaultVal);
336
                        p = SQLBase.unquoteStringStd(defaultVal);
337
                    else
337
                    else
338
                        this.parsedDefaultValue = Value.getNone();
338
                        this.parsedDefaultValue = Value.getNone();
339
                } else if (Number.class.isAssignableFrom(javaType)) {
339
                } else if (Number.class.isAssignableFrom(javaType)) {
340
                    p = new BigDecimal(defaultVal);
340
                    p = new BigDecimal(defaultVal);
341
                } else if (Boolean.class.isAssignableFrom(javaType)) {
341
                } else if (Boolean.class.isAssignableFrom(javaType)) {
342
                    p = Boolean.parseBoolean(defaultVal);
342
                    p = Boolean.parseBoolean(defaultVal);
343
                } else if (Timestamp.class.isAssignableFrom(javaType)) {
343
                } else if (Timestamp.class.isAssignableFrom(javaType)) {
344
                    p = Timestamp.valueOf(SQLBase.unquoteStringStd(defaultVal));
344
                    p = Timestamp.valueOf(SQLBase.unquoteStringStd(defaultVal));
345
                } else if (Time.class.isAssignableFrom(javaType)) {
345
                } else if (Time.class.isAssignableFrom(javaType)) {
346
                    p = Time.valueOf(SQLBase.unquoteStringStd(defaultVal));
346
                    p = Time.valueOf(SQLBase.unquoteStringStd(defaultVal));
347
                } else if (Date.class.isAssignableFrom(javaType)) {
347
                } else if (Date.class.isAssignableFrom(javaType)) {
348
                    p = java.sql.Date.valueOf(SQLBase.unquoteStringStd(defaultVal));
348
                    p = java.sql.Date.valueOf(SQLBase.unquoteStringStd(defaultVal));
349
                } else {
349
                } else {
350
                    throw new IllegalStateException("Unsupported type " + this.getType());
350
                    throw new IllegalStateException("Unsupported type " + this.getType());
351
                }
351
                }
352
                if (this.parsedDefaultValue == null)
352
                if (this.parsedDefaultValue == null)
353
                    this.parsedDefaultValue = Value.<Object> getSome(p);
353
                    this.parsedDefaultValue = Value.<Object> getSome(p);
354
            } catch (Exception e) {
354
            } catch (Exception e) {
355
                Log.get().log(Level.FINE, "Couldn't parse " + this.defaultValue, e);
355
                Log.get().log(Level.FINE, "Couldn't parse " + this.defaultValue, e);
356
                this.parsedDefaultValue = Value.getNone();
356
                this.parsedDefaultValue = Value.getNone();
357
            }
357
            }
358
            assert this.parsedDefaultValue != null;
358
            assert this.parsedDefaultValue != null;
359
        }
359
        }
360
        return this.parsedDefaultValue;
360
        return this.parsedDefaultValue;
361
    }
361
    }
362
 
362
 
363
    /**
363
    /**
364
     * Whether this field accepts NULL.
364
     * Whether this field accepts NULL.
365
     * 
365
     * 
366
     * @return <code>true</code> if it does, <code>false</code> if not, <code>null</code> if
366
     * @return <code>true</code> if it does, <code>false</code> if not, <code>null</code> if
367
     *         unknown.
367
     *         unknown.
368
     */
368
     */
369
    public synchronized final Boolean isNullable() {
369
    public synchronized final Boolean isNullable() {
370
        return this.nullable;
370
        return this.nullable;
371
    }
371
    }
372
 
372
 
373
    public boolean isKey() {
373
    public boolean isKey() {
374
        return this.isPrimaryKey() || this.isForeignKey();
374
        return this.isPrimaryKey() || this.isForeignKey();
375
    }
375
    }
376
 
376
 
377
    /**
377
    /**
378
     * Is this the one and only field in the primary key of its table.
378
     * Is this the one and only field in the primary key of its table.
379
     * 
379
     * 
380
     * @return <code>true</code> if this is part of the primary key, and the primary key has no
380
     * @return <code>true</code> if this is part of the primary key, and the primary key has no
381
     *         other fields.
381
     *         other fields.
382
     */
382
     */
383
    public boolean isPrimaryKey() {
383
    public boolean isPrimaryKey() {
384
        return this.getTable().getPrimaryKeys().equals(Collections.singleton(this));
384
        return this.getTable().getPrimaryKeys().equals(Collections.singleton(this));
385
    }
385
    }
386
 
386
 
387
    /**
387
    /**
388
     * Is this the one and only field in a foreign key of its table.
388
     * Is this the one and only field in a foreign key of its table.
389
     * 
389
     * 
390
     * @return <code>true</code> if this is part of a foreign key that has no other fields.
390
     * @return <code>true</code> if this is part of a foreign key that has no other fields.
391
     * @see #getFieldGroup()
391
     * @see #getFieldGroup()
392
     */
392
     */
393
    public boolean isForeignKey() {
393
    public boolean isForeignKey() {
394
        final FieldGroup fieldGroup = getContainingFieldGroup();
394
        final FieldGroup fieldGroup = getContainingFieldGroup();
395
        return fieldGroup.getKeyType() == Type.FOREIGN_KEY && fieldGroup.getSingleField() != null;
395
        return fieldGroup.getKeyType() == Type.FOREIGN_KEY && fieldGroup.getSingleField() != null;
396
    }
396
    }
397
 
397
 
398
    /**
398
    /**
399
     * To which group this field belong.
399
     * To which group this field belong.
400
     * 
400
     * 
401
     * @return the group of this field.
401
     * @return the group of this field.
402
     * @see SQLTable#getFieldGroups()
402
     * @see SQLTable#getFieldGroups()
403
     */
403
     */
404
    public FieldGroup getContainingFieldGroup() {
404
    public FieldGroup getContainingFieldGroup() {
405
        return this.getTable().getFieldGroups().get(this.getName());
405
        return this.getTable().getFieldGroups().get(this.getName());
406
    }
406
    }
407
 
407
 
408
    /**
408
    /**
409
     * The group consisting of this single field.
409
     * The group consisting of this single field.
410
     * 
410
     * 
411
     * @return the group consisting of this single field.
411
     * @return the group consisting of this single field.
412
     * @throws IllegalStateException if this field is part of a multi-field group.
412
     * @throws IllegalStateException if this field is part of a multi-field group.
413
     */
413
     */
414
    public FieldGroup getFieldGroup() throws IllegalStateException {
414
    public FieldGroup getFieldGroup() throws IllegalStateException {
415
        final FieldGroup fg = this.getTable().getFieldGroups().get(this.getName());
415
        final FieldGroup fg = this.getTable().getFieldGroups().get(this.getName());
416
        if (fg.getSingleField() == null)
416
        if (fg.getSingleField() == null)
417
            throw new IllegalStateException(this + " is part of a group with others : " + fg);
417
            throw new IllegalStateException(this + " is part of a group with others : " + fg);
418
        return fg;
418
        return fg;
419
    }
419
    }
420
 
420
 
421
    public final SQLTable getForeignTable() {
421
    public final SQLTable getForeignTable() {
422
        return this.getDBSystemRoot().getGraph().getForeignTable(this);
422
        return this.getDBSystemRoot().getGraph().getForeignTable(this);
423
    }
423
    }
424
 
424
 
425
    public final Link getLink() {
425
    public final Link getLink() {
426
        return this.getDBSystemRoot().getGraph().getForeignLink(this);
426
        return this.getDBSystemRoot().getGraph().getForeignLink(this);
427
    }
427
    }
428
 
428
 
429
    // *** FieldRef
429
    // *** FieldRef
430
 
430
 
431
    public SQLField getField() {
431
    public SQLField getField() {
432
        return this;
432
        return this;
433
    }
433
    }
434
 
434
 
435
    public String getFieldRef() {
435
    public String getFieldRef() {
436
        return SQLBase.quoteIdentifier(this.getAlias()) + "." + SQLBase.quoteIdentifier(this.getField().getName());
436
        return SQLBase.quoteIdentifier(this.getAlias()) + "." + SQLBase.quoteIdentifier(this.getField().getName());
437
    }
437
    }
438
 
438
 
439
    public String getAlias() {
439
    public String getAlias() {
440
        return this.getTable().getName();
440
        return this.getTable().getName();
441
    }
441
    }
442
 
442
 
443
    @Override
443
    @Override
444
    public TableRef getTableRef() {
444
    public TableRef getTableRef() {
445
        return this.getTable();
445
        return this.getTable();
446
    }
446
    }
447
 
447
 
448
    /**
448
    /**
449
     * Return this field in the passed table.
449
     * Return this field in the passed table.
450
     * 
450
     * 
451
     * @param table a table, e.g OBSERVATION obs.
451
     * @param table a table, e.g OBSERVATION obs.
452
     * @return a field in the passed table, e.g. if this is OBSERVATION.DESIGNATION then
452
     * @return a field in the passed table, e.g. if this is OBSERVATION.DESIGNATION then
453
     *         obs.DESIGNATION.
453
     *         obs.DESIGNATION.
454
     * @throws IllegalArgumentException if this field is not in the same table as the argument.
454
     * @throws IllegalArgumentException if this field is not in the same table as the argument.
455
     * @see {@link TableRef#getField(String)}
455
     * @see {@link TableRef#getField(String)}
456
     */
456
     */
457
    public final FieldRef getFieldRef(TableRef table) throws IllegalArgumentException {
457
    public final FieldRef getFieldRef(TableRef table) throws IllegalArgumentException {
458
        if (table.getTable() != this.getTable())
458
        if (table.getTable() != this.getTable())
459
            throw new IllegalArgumentException("Table mismatch for " + table + " and " + this);
459
            throw new IllegalArgumentException("Table mismatch for " + table + " and " + this);
460
        return table.getField(this.getName());
460
        return table.getField(this.getName());
461
    }
461
    }
462
 
462
 
463
    public synchronized String toXML() {
463
    public synchronized String toXML() {
464
        if (this.xml == null) {
464
        if (this.xml == null) {
465
            final StringBuilder sb = new StringBuilder(2048);
465
            final StringBuilder sb = new StringBuilder(2048);
466
            sb.append("<field name=\"");
466
            sb.append("<field name=\"");
467
            sb.append(JDOM2Utils.OUTPUTTER.escapeAttributeEntities(this.getName()));
467
            sb.append(JDOM2Utils.OUTPUTTER.escapeAttributeEntities(this.getName()));
468
            sb.append("\" >");
468
            sb.append("\" >");
469
            sb.append(this.type.toXML());
469
            sb.append(this.type.toXML());
470
            sb.append(XMLCodecUtils.encodeSimple(this.metadata));
470
            sb.append(XMLCodecUtils.encodeSimple(this.metadata));
471
            sb.append("<infoSchema>");
471
            sb.append("<infoSchema>");
472
            sb.append(XMLCodecUtils.encodeSimple(this.infoSchemaCols));
472
            sb.append(XMLCodecUtils.encodeSimple(this.infoSchemaCols));
473
            sb.append("</infoSchema></field>\n");
473
            sb.append("</infoSchema></field>\n");
474
            this.xml = sb.toString();
474
            this.xml = sb.toString();
475
        }
475
        }
476
        return this.xml;
476
        return this.xml;
477
    }
477
    }
478
 
478
 
479
    @Override
479
    @Override
480
    public Map<String, ? extends DBStructureItemJDBC> getChildrenMap() {
480
    public Map<String, ? extends DBStructureItemJDBC> getChildrenMap() {
481
        return Collections.emptyMap();
481
        return Collections.emptyMap();
482
    }
482
    }
483
 
483
 
484
    // MAYBE equalsDesc in DBStructureItem
484
    // MAYBE equalsDesc in DBStructureItem
485
    public boolean equalsDesc(SQLField o) {
485
    public boolean equalsDesc(SQLField o) {
486
        return this.equalsDesc(o, null, true) == null;
486
        return this.equalsDesc(o, null, true) == null;
487
    }
487
    }
488
 
488
 
489
    // compareDefault useful when fields' default are functions containing the name of the table (eg
489
    // compareDefault useful when fields' default are functions containing the name of the table (eg
490
    // serial)
490
    // serial)
491
    public String equalsDesc(SQLField o, SQLSystem otherSystem, boolean compareDefault) {
491
    public String equalsDesc(SQLField o, SQLSyntax otherSyntax, boolean compareDefault) {
492
        final Map<Properties, String> res = getDiffMap(o, otherSystem, compareDefault);
492
        final Map<Properties, String> res = getDiffMap(o, otherSyntax, compareDefault);
493
        if (res.size() == 0)
493
        if (res.size() == 0)
494
            return null;
494
            return null;
495
        else
495
        else
496
            return this.getSQLName() + " != " + o.getSQLName() + ":\n" + CollectionUtils.join(res.values(), "\n");
496
            return this.getSQLName() + " != " + o.getSQLName() + ":\n" + CollectionUtils.join(res.values(), "\n");
497
    }
497
    }
498
 
498
 
499
    /**
499
    /**
500
     * Return the differences between this and <code>o</code>.
500
     * Return the differences between this and <code>o</code>.
501
     * 
501
     * 
502
     * @param o another field.
502
     * @param o another field.
503
     * @param otherSystem the system <code>o</code> originated from, can be <code>null</code>.
503
     * @param otherSystem the system <code>o</code> originated from, can be <code>null</code>.
504
     * @param compareDefault <code>true</code> if defaults should be compared.
504
     * @param compareDefault <code>true</code> if defaults should be compared.
505
     * @return a map containing properties that differs and their values.
505
     * @return a map containing properties that differs and their values.
506
     */
506
     */
507
    public synchronized Map<Properties, String> getDiffMap(SQLField o, SQLSystem otherSystem, boolean compareDefault) {
507
    public synchronized Map<Properties, String> getDiffMap(SQLField o, SQLSyntax otherSystem, boolean compareDefault) {
508
        if (o == null)
508
        if (o == null)
509
            return Collections.singletonMap(null, "other field is null");
509
            return Collections.singletonMap(null, "other field is null");
510
        final Map<Properties, String> res = new HashMap<Properties, String>();
510
        final Map<Properties, String> res = new HashMap<Properties, String>();
511
        if (!this.getName().equals(o.getName()))
511
        if (!this.getName().equals(o.getName()))
512
            res.put(Properties.NAME, "name unequal : " + quoteIdentifier(this.getName()) + " != " + quoteIdentifier(o.getName()));
512
            res.put(Properties.NAME, "name unequal : " + quoteIdentifier(this.getName()) + " != " + quoteIdentifier(o.getName()));
513
        if (!this.getType().equals(o.getType(), otherSystem))
513
        if (!this.getType().equals(o.getType(), otherSystem))
514
            res.put(Properties.TYPE, "type unequal : " + this.getType() + " " + o.getType());
514
            res.put(Properties.TYPE, "type unequal : " + this.getType() + " " + o.getType());
515
        if (!CompareUtils.equals(this.isNullable(), o.isNullable()))
515
        if (!CompareUtils.equals(this.isNullable(), o.isNullable()))
516
            res.put(Properties.NULLABLE, "is_nullable unequal : " + this.isNullable() + " " + o.isNullable());
516
            res.put(Properties.NULLABLE, "is_nullable unequal : " + this.isNullable() + " " + o.isNullable());
517
        if (compareDefault && !defaultEquals(o))
517
        if (compareDefault && !defaultEquals(o))
518
            res.put(Properties.DEFAULT, "default unequal : " + print(this.getDefaultValue()) + " != " + print(o.getDefaultValue()));
518
            res.put(Properties.DEFAULT, "default unequal : " + print(this.getDefaultValue()) + " != " + print(o.getDefaultValue()));
519
        return res;
519
        return res;
520
    }
520
    }
521
 
521
 
522
    private boolean defaultEquals(SQLField o) {
522
    private boolean defaultEquals(SQLField o) {
523
        final SQLSyntax syntax = this.getDBSystemRoot().getSyntax();
523
        final SQLSyntax syntax = this.getDBSystemRoot().getSyntax();
524
        final SQLSyntax oSyntax = o.getDBSystemRoot().getSyntax();
524
        final SQLSyntax oSyntax = o.getDBSystemRoot().getSyntax();
525
        // don't compare actual default for auto fields, e.g. on MySQL it's null on PG it
525
        // don't compare actual default for auto fields, e.g. on MySQL it's null on PG it
526
        // nextval(seq)
526
        // nextval(seq)
527
        if (syntax.isAuto(this) && oSyntax.isAuto(o))
527
        if (syntax.isAuto(this) && oSyntax.isAuto(o))
528
            return true;
528
            return true;
529
        // normalize to this syntax before doing a string comparison
529
        // normalize to this syntax before doing a string comparison
530
        // perhaps: if that comparison fails, execute "SELECT default" and compare the java objects.
530
        // perhaps: if that comparison fails, execute "SELECT default" and compare the java objects.
531
        return CompareUtils.equals(normalizeDefault(this, syntax), normalizeDefault(o, syntax));
531
        return CompareUtils.equals(normalizeDefault(this, syntax), normalizeDefault(o, syntax));
532
    }
532
    }
533
 
533
 
534
    private static String normalizeDefault(SQLField f, final SQLSyntax syntax) {
534
    private static String normalizeDefault(SQLField f, final SQLSyntax syntax) {
535
        final String def = syntax.getDefault(f);
535
        final String def = syntax.getDefault(f);
536
        // no explicit default and DEFAULT NULL is equivalent
536
        // no explicit default and DEFAULT NULL is equivalent
537
        return def != null && def.trim().toUpperCase().equals("NULL") ? null : def;
537
        return def != null && def.trim().toUpperCase().equals("NULL") ? null : def;
538
    }
538
    }
539
 
539
 
540
    // disambiguate NULL
540
    // disambiguate NULL
541
    private static String print(Object o) {
541
    private static String print(Object o) {
542
        return o == null ? "NULL" : "<" + o + ">";
542
        return o == null ? "NULL" : "<" + o + ">";
543
    }
543
    }
544
 
544
 
545
    // IFieldPath
545
    // IFieldPath
546
 
546
 
547
    @Override
547
    @Override
548
    public FieldPath getFieldPath() {
548
    public FieldPath getFieldPath() {
549
        return new FieldPath(this);
549
        return new FieldPath(this);
550
    }
550
    }
551
 
551
 
552
    @Override
552
    @Override
553
    public Path getPath() {
553
    public Path getPath() {
554
        return Path.get(getTable());
554
        return Path.get(getTable());
555
    }
555
    }
556
 
556
 
557
    @Override
557
    @Override
558
    public String getFieldName() {
558
    public String getFieldName() {
559
        return this.getName();
559
        return this.getName();
560
    }
560
    }
561
}
561
}