17 |
ilm |
1 |
/*
|
|
|
2 |
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
|
|
|
3 |
*
|
182 |
ilm |
4 |
* Copyright 2011-2019 OpenConcerto, by ILM Informatique. All rights reserved.
|
17 |
ilm |
5 |
*
|
|
|
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
|
|
|
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.
|
|
|
10 |
*
|
|
|
11 |
* When distributing the software, include this License Header Notice in each file.
|
|
|
12 |
*/
|
|
|
13 |
|
|
|
14 |
/*
|
|
|
15 |
* DataBase created on 4 mai 2004
|
|
|
16 |
*/
|
|
|
17 |
package org.openconcerto.sql.model;
|
|
|
18 |
|
|
|
19 |
import org.openconcerto.sql.Log;
|
63 |
ilm |
20 |
import org.openconcerto.sql.model.LoadingListener.LoadingEvent;
|
|
|
21 |
import org.openconcerto.sql.model.LoadingListener.StructureLoadingEvent;
|
17 |
ilm |
22 |
import org.openconcerto.sql.model.graph.DatabaseGraph;
|
67 |
ilm |
23 |
import org.openconcerto.sql.model.graph.TablesMap;
|
|
|
24 |
import org.openconcerto.sql.utils.SQLUtils;
|
17 |
ilm |
25 |
import org.openconcerto.utils.CollectionUtils;
|
|
|
26 |
import org.openconcerto.utils.FileUtils;
|
61 |
ilm |
27 |
import org.openconcerto.utils.Tuple3;
|
|
|
28 |
import org.openconcerto.utils.cc.CopyOnWriteMap;
|
17 |
ilm |
29 |
import org.openconcerto.utils.cc.IClosure;
|
83 |
ilm |
30 |
import org.openconcerto.utils.cc.ITransformer;
|
17 |
ilm |
31 |
import org.openconcerto.utils.change.CollectionChangeEventCreator;
|
|
|
32 |
|
|
|
33 |
import java.io.File;
|
67 |
ilm |
34 |
import java.io.IOException;
|
|
|
35 |
import java.io.Writer;
|
80 |
ilm |
36 |
import java.security.AccessController;
|
|
|
37 |
import java.security.PrivilegedAction;
|
17 |
ilm |
38 |
import java.sql.DatabaseMetaData;
|
|
|
39 |
import java.sql.ResultSet;
|
|
|
40 |
import java.sql.SQLException;
|
83 |
ilm |
41 |
import java.util.Collection;
|
17 |
ilm |
42 |
import java.util.Collections;
|
|
|
43 |
import java.util.HashMap;
|
|
|
44 |
import java.util.HashSet;
|
83 |
ilm |
45 |
import java.util.LinkedHashMap;
|
17 |
ilm |
46 |
import java.util.List;
|
|
|
47 |
import java.util.Map;
|
73 |
ilm |
48 |
import java.util.Map.Entry;
|
182 |
ilm |
49 |
import java.util.Objects;
|
17 |
ilm |
50 |
import java.util.Set;
|
65 |
ilm |
51 |
import java.util.logging.Level;
|
|
|
52 |
import java.util.logging.Logger;
|
17 |
ilm |
53 |
import java.util.regex.Matcher;
|
|
|
54 |
import java.util.regex.Pattern;
|
|
|
55 |
|
142 |
ilm |
56 |
import org.apache.commons.dbutils.ResultSetHandler;
|
|
|
57 |
|
61 |
ilm |
58 |
import net.jcip.annotations.GuardedBy;
|
|
|
59 |
import net.jcip.annotations.ThreadSafe;
|
|
|
60 |
|
17 |
ilm |
61 |
/**
|
|
|
62 |
* Une base de donnée SQL. Une base est unique, pour obtenir une instance il faut passer par
|
|
|
63 |
* SQLServer. Une base permet d'accéder aux tables qui la composent, ainsi qu'à son graphe.
|
|
|
64 |
*
|
|
|
65 |
* @author ILM Informatique 4 mai 2004
|
61 |
ilm |
66 |
* @see org.openconcerto.sql.model.SQLServer#getOrCreateBase(String)
|
17 |
ilm |
67 |
* @see #getTable(String)
|
|
|
68 |
* @see #getGraph()
|
|
|
69 |
*/
|
61 |
ilm |
70 |
@ThreadSafe
|
142 |
ilm |
71 |
public final class SQLBase extends SQLIdentifier {
|
17 |
ilm |
72 |
|
|
|
73 |
/**
|
|
|
74 |
* Boolean system property, if <code>true</code> then the structure and the graph of SQL base
|
67 |
ilm |
75 |
* will default to be loaded from XML instead of JDBC.
|
|
|
76 |
*
|
|
|
77 |
* @see DBSystemRoot#useCache()
|
17 |
ilm |
78 |
*/
|
|
|
79 |
public static final String STRUCTURE_USE_XML = "org.openconcerto.sql.structure.useXML";
|
19 |
ilm |
80 |
/**
|
67 |
ilm |
81 |
* Boolean system property, if <code>true</code> then when the structure of SQL base cannot be
|
|
|
82 |
* loaded from XML, the files are not deleted.
|
|
|
83 |
*/
|
|
|
84 |
public static final String STRUCTURE_KEEP_INVALID_XML = "org.openconcerto.sql.structure.keepInvalidXML";
|
|
|
85 |
/**
|
19 |
ilm |
86 |
* Boolean system property, if <code>true</code> then schemas and tables can be dropped,
|
67 |
ilm |
87 |
* otherwise the refresh will throw an exception.
|
19 |
ilm |
88 |
*/
|
|
|
89 |
public static final String ALLOW_OBJECT_REMOVAL = "org.openconcerto.sql.identifier.allowRemoval";
|
17 |
ilm |
90 |
|
20 |
ilm |
91 |
static public final void logCacheError(final DBItemFileCache dir, Exception e) {
|
65 |
ilm |
92 |
final Logger logger = Log.get();
|
|
|
93 |
if (logger.isLoggable(Level.CONFIG))
|
|
|
94 |
logger.log(Level.CONFIG, "invalid files in " + dir, e);
|
|
|
95 |
else
|
|
|
96 |
logger.info("invalid files in " + dir + "\n" + e.getMessage());
|
20 |
ilm |
97 |
}
|
|
|
98 |
|
17 |
ilm |
99 |
// null is a valid name (MySQL doesn't support schemas)
|
61 |
ilm |
100 |
private final CopyOnWriteMap<String, SQLSchema> schemas;
|
|
|
101 |
@GuardedBy("this")
|
17 |
ilm |
102 |
private int[] dbVersion;
|
|
|
103 |
|
|
|
104 |
/**
|
|
|
105 |
* Crée une base dans <i>server </i> nommée <i>name </i>.
|
|
|
106 |
* <p>
|
61 |
ilm |
107 |
* Note: ne pas utiliser ce constructeur, utiliser {@link SQLServer#getOrCreateBase(String)}
|
17 |
ilm |
108 |
* </p>
|
|
|
109 |
*
|
|
|
110 |
* @param server son serveur.
|
|
|
111 |
* @param name son nom.
|
|
|
112 |
* @param login the login.
|
|
|
113 |
* @param pass the password.
|
|
|
114 |
*/
|
|
|
115 |
SQLBase(SQLServer server, String name, String login, String pass) {
|
83 |
ilm |
116 |
this(server, name, null, login, pass, null);
|
17 |
ilm |
117 |
}
|
|
|
118 |
|
|
|
119 |
/**
|
|
|
120 |
* Creates a base in <i>server</i> named <i>name</i>.
|
|
|
121 |
* <p>
|
61 |
ilm |
122 |
* Note: don't use this constructor, use {@link SQLServer#getOrCreateBase(String)}
|
17 |
ilm |
123 |
* </p>
|
|
|
124 |
*
|
|
|
125 |
* @param server its server.
|
|
|
126 |
* @param name its name.
|
83 |
ilm |
127 |
* @param systemRootInit to initialize the {@link DBSystemRoot} before setting the datasource.
|
17 |
ilm |
128 |
* @param login the login.
|
|
|
129 |
* @param pass the password.
|
|
|
130 |
* @param dsInit to initialize the datasource before any request (eg setting jdbc properties),
|
|
|
131 |
* can be <code>null</code>.
|
|
|
132 |
*/
|
83 |
ilm |
133 |
SQLBase(SQLServer server, String name, IClosure<? super DBSystemRoot> systemRootInit, String login, String pass, IClosure<? super SQLDataSource> dsInit) {
|
17 |
ilm |
134 |
super(server, name);
|
|
|
135 |
if (name == null)
|
|
|
136 |
throw new NullPointerException("null base");
|
61 |
ilm |
137 |
this.schemas = new CopyOnWriteMap<String, SQLSchema>();
|
17 |
ilm |
138 |
this.dbVersion = null;
|
|
|
139 |
|
|
|
140 |
// if this is the systemRoot we must init the datasource to be able to loadTables()
|
|
|
141 |
final DBSystemRoot sysRoot = this.getDBSystemRoot();
|
|
|
142 |
if (sysRoot.getJDBC() == this)
|
83 |
ilm |
143 |
sysRoot.setDS(systemRootInit, login, pass, dsInit);
|
61 |
ilm |
144 |
}
|
17 |
ilm |
145 |
|
67 |
ilm |
146 |
final TablesMap init(final boolean readCache) {
|
17 |
ilm |
147 |
try {
|
67 |
ilm |
148 |
return refresh(null, readCache, true);
|
17 |
ilm |
149 |
} catch (SQLException e) {
|
65 |
ilm |
150 |
throw new IllegalStateException("could not init " + this, e);
|
17 |
ilm |
151 |
}
|
|
|
152 |
}
|
|
|
153 |
|
|
|
154 |
@Override
|
61 |
ilm |
155 |
protected synchronized void onDrop() {
|
17 |
ilm |
156 |
// allow schemas (and their descendants) to be gc'd even we aren't
|
|
|
157 |
this.schemas.clear();
|
|
|
158 |
super.onDrop();
|
|
|
159 |
}
|
|
|
160 |
|
67 |
ilm |
161 |
TablesMap refresh(final TablesMap namesToRefresh, final boolean readCache) throws SQLException {
|
|
|
162 |
return this.refresh(namesToRefresh, readCache, false);
|
65 |
ilm |
163 |
}
|
|
|
164 |
|
67 |
ilm |
165 |
// what tables were loaded by JDBC
|
|
|
166 |
private TablesMap refresh(final TablesMap namesToRefresh, final boolean readCache, final boolean inCtor) throws SQLException {
|
65 |
ilm |
167 |
if (readCache)
|
67 |
ilm |
168 |
return loadTables(namesToRefresh, inCtor);
|
65 |
ilm |
169 |
else
|
67 |
ilm |
170 |
return fetchTables(namesToRefresh);
|
65 |
ilm |
171 |
}
|
|
|
172 |
|
67 |
ilm |
173 |
private final TablesMap loadTables(TablesMap childrenNames, boolean inCtor) throws SQLException {
|
61 |
ilm |
174 |
this.checkDropped();
|
67 |
ilm |
175 |
if (childrenNames != null && childrenNames.size() == 0)
|
|
|
176 |
return childrenNames;
|
|
|
177 |
childrenNames = assureAllTables(childrenNames);
|
17 |
ilm |
178 |
final DBItemFileCache dir = getFileCache();
|
61 |
ilm |
179 |
synchronized (getTreeMutex()) {
|
67 |
ilm |
180 |
XMLStructureSource xmlStructSrc = null;
|
61 |
ilm |
181 |
if (dir != null) {
|
|
|
182 |
try {
|
|
|
183 |
Log.get().config("for mapping " + this + " trying xmls in " + dir);
|
|
|
184 |
final long t1 = System.currentTimeMillis();
|
|
|
185 |
// don't call refreshTables() with XML :
|
|
|
186 |
// say you have one schema "s" and its file is missing or corrupted
|
|
|
187 |
// refreshTables(XML) will drop it from our children
|
|
|
188 |
// then we will call refreshTables(JDBC) and it will be re-added
|
|
|
189 |
// => so we removed our child for nothing (firing unneeded events, rendering
|
|
|
190 |
// java objects useless and possibly destroying the systemRoot path)
|
67 |
ilm |
191 |
xmlStructSrc = new XMLStructureSource(this, childrenNames, dir);
|
|
|
192 |
assert xmlStructSrc.isPreVerify();
|
61 |
ilm |
193 |
xmlStructSrc.init();
|
|
|
194 |
final long t2 = System.currentTimeMillis();
|
|
|
195 |
Log.get().config("XML took " + (t2 - t1) + "ms for mapping " + this.getName() + "." + xmlStructSrc.getSchemas());
|
|
|
196 |
} catch (Exception e) {
|
|
|
197 |
logCacheError(dir, e);
|
67 |
ilm |
198 |
// since isPreVerify() is true, schemas weren't changed.
|
|
|
199 |
// if an error reached us, we cannot trust the loaded structure (e.g.
|
|
|
200 |
// IOExceptions are handled by XMLStructureSource)
|
|
|
201 |
xmlStructSrc = null;
|
17 |
ilm |
202 |
}
|
|
|
203 |
}
|
61 |
ilm |
204 |
|
|
|
205 |
final long t1 = System.currentTimeMillis();
|
|
|
206 |
// always do the fetchTables() since XML do nothing anymore
|
67 |
ilm |
207 |
final JDBCStructureSource jdbcStructSrc = this.fetchTablesP(childrenNames, xmlStructSrc);
|
61 |
ilm |
208 |
final long t2 = System.currentTimeMillis();
|
|
|
209 |
Log.get().config("JDBC took " + (t2 - t1) + "ms for mapping " + this.getName() + "." + jdbcStructSrc.getSchemas());
|
67 |
ilm |
210 |
return jdbcStructSrc.getTablesMap();
|
17 |
ilm |
211 |
}
|
|
|
212 |
}
|
|
|
213 |
|
67 |
ilm |
214 |
private final TablesMap assureAllTables(final TablesMap childrenNames) {
|
|
|
215 |
// don't allow partial schemas (we do the same in SQLServer.refresh()) since
|
|
|
216 |
// JDBCStructureSource needs to check for SQLSchema.METADATA_TABLENAME
|
|
|
217 |
final TablesMap res;
|
|
|
218 |
if (childrenNames == null) {
|
|
|
219 |
res = childrenNames;
|
|
|
220 |
} else {
|
|
|
221 |
res = TablesMap.create(childrenNames);
|
|
|
222 |
for (final Entry<String, Set<String>> e : childrenNames.entrySet()) {
|
|
|
223 |
final String schemaName = e.getKey();
|
|
|
224 |
if (e.getValue() != null && !this.contains(schemaName)) {
|
|
|
225 |
res.put(schemaName, null);
|
|
|
226 |
}
|
|
|
227 |
}
|
|
|
228 |
}
|
|
|
229 |
return res;
|
17 |
ilm |
230 |
}
|
|
|
231 |
|
|
|
232 |
/**
|
|
|
233 |
* Load the structure from JDBC.
|
|
|
234 |
*
|
67 |
ilm |
235 |
* @param childrenNames which children to refresh, <code>null</code> meaning all.
|
|
|
236 |
* @return tables actually loaded, never <code>null</code>.
|
17 |
ilm |
237 |
* @throws SQLException if an error occurs.
|
|
|
238 |
* @see DBSystemRoot#refetch(Set)
|
|
|
239 |
*/
|
67 |
ilm |
240 |
TablesMap fetchTables(TablesMap childrenNames) throws SQLException {
|
|
|
241 |
if (childrenNames != null && childrenNames.size() == 0)
|
|
|
242 |
return childrenNames;
|
|
|
243 |
return this.fetchTablesP(assureAllTables(childrenNames), null).getTablesMap();
|
17 |
ilm |
244 |
}
|
|
|
245 |
|
67 |
ilm |
246 |
private JDBCStructureSource fetchTablesP(TablesMap childrenNames, StructureSource<?> external) throws SQLException {
|
|
|
247 |
// TODO pass TablesByRoot to event
|
|
|
248 |
final LoadingEvent evt = new StructureLoadingEvent(this, childrenNames == null ? null : childrenNames.keySet());
|
63 |
ilm |
249 |
final DBSystemRoot sysRoot = this.getDBSystemRoot();
|
|
|
250 |
try {
|
|
|
251 |
sysRoot.fireLoading(evt);
|
67 |
ilm |
252 |
return this.refreshTables(new JDBCStructureSource(this, childrenNames, external == null ? null : external.getNewStructure(), external == null ? null : external.getOutOfDateSchemas()));
|
63 |
ilm |
253 |
} finally {
|
|
|
254 |
sysRoot.fireLoading(evt.createFinishingEvent());
|
|
|
255 |
}
|
17 |
ilm |
256 |
}
|
|
|
257 |
|
67 |
ilm |
258 |
final TablesMap loadTables() throws SQLException {
|
17 |
ilm |
259 |
return this.loadTables(null);
|
|
|
260 |
}
|
|
|
261 |
|
|
|
262 |
/**
|
|
|
263 |
* Tries to load the structure from XMLs, if that fails fallback to JDBC.
|
|
|
264 |
*
|
|
|
265 |
* @param childrenNames which children to refresh.
|
67 |
ilm |
266 |
* @return tables loaded with JDBC.
|
17 |
ilm |
267 |
* @throws SQLException if an error occurs in JDBC.
|
|
|
268 |
*/
|
67 |
ilm |
269 |
final TablesMap loadTables(TablesMap childrenNames) throws SQLException {
|
17 |
ilm |
270 |
return this.loadTables(childrenNames, false);
|
|
|
271 |
}
|
|
|
272 |
|
61 |
ilm |
273 |
private final <T extends Exception, S extends StructureSource<T>> S refreshTables(final S src) throws T {
|
|
|
274 |
this.checkDropped();
|
|
|
275 |
synchronized (getTreeMutex()) {
|
|
|
276 |
src.init();
|
17 |
ilm |
277 |
|
61 |
ilm |
278 |
// refresh schemas
|
|
|
279 |
final Set<String> newSchemas = src.getTotalSchemas();
|
67 |
ilm |
280 |
final Set<String> currentSchemas = src.getExistingSchemasToRefresh();
|
61 |
ilm |
281 |
mustContain(this, newSchemas, currentSchemas, "schemas");
|
80 |
ilm |
282 |
final CollectionChangeEventCreator c = this.createChildrenCreator();
|
61 |
ilm |
283 |
// remove all schemas that are not there anymore
|
|
|
284 |
for (final String schema : CollectionUtils.substract(currentSchemas, newSchemas)) {
|
|
|
285 |
this.schemas.remove(schema).dropped();
|
17 |
ilm |
286 |
}
|
61 |
ilm |
287 |
// delete the saved schemas that we could have fetched, but haven't
|
|
|
288 |
// (schemas that are not in scope are simply ignored, NOT deleted)
|
80 |
ilm |
289 |
AccessController.doPrivileged(new PrivilegedAction<Object>() {
|
|
|
290 |
@Override
|
|
|
291 |
public Object run() {
|
|
|
292 |
for (final DBItemFileCache savedSchema : getSavedCaches(false)) {
|
|
|
293 |
if (src.isInTotalScope(savedSchema.getName()) && !newSchemas.contains(savedSchema.getName())) {
|
|
|
294 |
savedSchema.delete();
|
|
|
295 |
}
|
|
|
296 |
}
|
|
|
297 |
return null;
|
61 |
ilm |
298 |
}
|
80 |
ilm |
299 |
});
|
|
|
300 |
|
61 |
ilm |
301 |
// clearNonPersistent (will be recreated by fillTables())
|
|
|
302 |
for (final String schema : CollectionUtils.inter(currentSchemas, newSchemas)) {
|
|
|
303 |
this.getSchema(schema).clearNonPersistent();
|
|
|
304 |
}
|
|
|
305 |
// create the new ones
|
|
|
306 |
for (final String schema : newSchemas) {
|
|
|
307 |
this.createAndGetSchema(schema);
|
|
|
308 |
}
|
17 |
ilm |
309 |
|
61 |
ilm |
310 |
// refresh tables
|
|
|
311 |
final Set<SQLName> newTableNames = src.getTotalTablesNames();
|
67 |
ilm |
312 |
final Set<SQLName> currentTables = src.getExistingTablesToRefresh();
|
61 |
ilm |
313 |
// we can only add, cause instances of SQLTable are everywhere
|
|
|
314 |
mustContain(this, newTableNames, currentTables, "tables");
|
|
|
315 |
// remove dropped tables
|
|
|
316 |
for (final SQLName tableName : CollectionUtils.substract(currentTables, newTableNames)) {
|
|
|
317 |
final SQLSchema s = this.getSchema(tableName.getItemLenient(-2));
|
|
|
318 |
s.rmTable(tableName.getName());
|
|
|
319 |
}
|
|
|
320 |
// clearNonPersistent
|
|
|
321 |
for (final SQLName tableName : CollectionUtils.inter(newTableNames, currentTables)) {
|
|
|
322 |
final SQLSchema s = this.getSchema(tableName.getItemLenient(-2));
|
|
|
323 |
s.getTable(tableName.getName()).clearNonPersistent();
|
|
|
324 |
}
|
|
|
325 |
// create new table descendants (including empty tables)
|
|
|
326 |
for (final SQLName tableName : CollectionUtils.substract(newTableNames, currentTables)) {
|
|
|
327 |
final SQLSchema s = this.getSchema(tableName.getItemLenient(-2));
|
|
|
328 |
s.addTable(tableName.getName());
|
|
|
329 |
}
|
17 |
ilm |
330 |
|
61 |
ilm |
331 |
// fill with columns
|
|
|
332 |
src.fillTables();
|
17 |
ilm |
333 |
|
80 |
ilm |
334 |
this.fireChildrenChanged(c);
|
61 |
ilm |
335 |
// don't signal our systemRoot if our server doesn't yet reference us,
|
|
|
336 |
// otherwise the server will create another instance and enter an infinite loop
|
|
|
337 |
assert this.getServer().getBase(this.getName()) == this;
|
67 |
ilm |
338 |
final TablesMap byRoot;
|
|
|
339 |
final TablesMap toRefresh = src.getToRefresh();
|
|
|
340 |
if (toRefresh == null) {
|
|
|
341 |
byRoot = TablesMap.createByRootFromChildren(this, null);
|
|
|
342 |
} else {
|
|
|
343 |
final DBRoot root = this.getDBRoot();
|
|
|
344 |
if (root != null) {
|
|
|
345 |
byRoot = TablesMap.createFromTables(root.getName(), toRefresh.get(null));
|
|
|
346 |
} else {
|
|
|
347 |
byRoot = toRefresh;
|
|
|
348 |
}
|
|
|
349 |
}
|
80 |
ilm |
350 |
this.getDBSystemRoot().descendantsChanged(byRoot, src.hasExternalStruct());
|
61 |
ilm |
351 |
}
|
17 |
ilm |
352 |
src.save();
|
|
|
353 |
return src;
|
|
|
354 |
}
|
|
|
355 |
|
|
|
356 |
static <T> void mustContain(final DBStructureItemJDBC c, final Set<T> newC, final Set<T> oldC, final String name) {
|
19 |
ilm |
357 |
if (Boolean.getBoolean(ALLOW_OBJECT_REMOVAL))
|
17 |
ilm |
358 |
return;
|
|
|
359 |
|
|
|
360 |
final Set<T> diff = CollectionUtils.contains(newC, oldC);
|
|
|
361 |
if (diff != null)
|
|
|
362 |
throw new IllegalStateException("some " + name + " were removed in " + c + ": " + diff);
|
|
|
363 |
}
|
|
|
364 |
|
|
|
365 |
public final String getURL() {
|
|
|
366 |
return this.getServer().getURL(this.getName());
|
|
|
367 |
}
|
|
|
368 |
|
|
|
369 |
/**
|
|
|
370 |
* Return the field named <i>fieldName </i> in this base.
|
|
|
371 |
*
|
|
|
372 |
* @param fieldName the fully qualified name of the field.
|
|
|
373 |
* @return the matching field or null if none exists.
|
65 |
ilm |
374 |
* @deprecated use {@link SQLTable#getField(String)} and {@link DBRoot#getTable(String)} or at
|
|
|
375 |
* worst {@link #getTable(SQLName)}
|
17 |
ilm |
376 |
*/
|
|
|
377 |
public SQLField getField(String fieldName) {
|
|
|
378 |
String[] parts = fieldName.split("\\.");
|
|
|
379 |
if (parts.length != 2) {
|
|
|
380 |
throw new IllegalArgumentException(fieldName + " is not a fully qualified name (like TABLE.FIELD_NAME).");
|
|
|
381 |
}
|
|
|
382 |
String table = parts[0];
|
|
|
383 |
String field = parts[1];
|
|
|
384 |
if (!this.containsTable(table))
|
|
|
385 |
return null;
|
|
|
386 |
else
|
|
|
387 |
return this.getTable(table).getField(field);
|
|
|
388 |
}
|
|
|
389 |
|
|
|
390 |
/**
|
|
|
391 |
* Return the table named <i>tablename </i> in this base.
|
|
|
392 |
*
|
|
|
393 |
* @param tablename the name of the table.
|
|
|
394 |
* @return the matching table or null if none exists.
|
|
|
395 |
*/
|
|
|
396 |
public SQLTable getTable(String tablename) {
|
|
|
397 |
return this.getTable(SQLName.parse(tablename));
|
|
|
398 |
}
|
|
|
399 |
|
|
|
400 |
public SQLTable getTable(SQLName n) {
|
|
|
401 |
if (n.getItemCount() == 0 || n.getItemCount() > 2)
|
|
|
402 |
throw new IllegalArgumentException("'" + n + "' is not a dotted tablename");
|
|
|
403 |
|
|
|
404 |
if (n.getItemCount() == 1) {
|
|
|
405 |
return this.findTable(n.getName());
|
|
|
406 |
} else {
|
|
|
407 |
final SQLSchema s = this.getSchema(n.getFirst());
|
|
|
408 |
if (s == null)
|
|
|
409 |
return null;
|
|
|
410 |
else
|
|
|
411 |
return s.getTable(n.getName());
|
|
|
412 |
}
|
|
|
413 |
}
|
|
|
414 |
|
|
|
415 |
private SQLTable findTable(String name) {
|
|
|
416 |
final DBRoot guessed = this.guessDBRoot();
|
|
|
417 |
return guessed == null ? this.getDBSystemRoot().findTable(name) : guessed.findTable(name);
|
|
|
418 |
}
|
|
|
419 |
|
|
|
420 |
/**
|
|
|
421 |
* Return whether this base contains the table.
|
|
|
422 |
*
|
|
|
423 |
* @param tableName the name of the table.
|
|
|
424 |
* @return true if the tableName exists.
|
|
|
425 |
*/
|
|
|
426 |
public boolean containsTable(String tableName) {
|
|
|
427 |
return contains(SQLName.parse(tableName));
|
|
|
428 |
}
|
|
|
429 |
|
|
|
430 |
private boolean contains(final SQLName n) {
|
|
|
431 |
return this.getTable(n) != null;
|
|
|
432 |
}
|
|
|
433 |
|
|
|
434 |
/**
|
|
|
435 |
* Return the tables in the default schema.
|
|
|
436 |
*
|
|
|
437 |
* @return an unmodifiable Set of the tables' names.
|
|
|
438 |
*/
|
|
|
439 |
public Set<String> getTableNames() {
|
|
|
440 |
return this.getDefaultSchema().getTableNames();
|
|
|
441 |
}
|
|
|
442 |
|
|
|
443 |
/**
|
|
|
444 |
* Return the tables in the default schema.
|
|
|
445 |
*
|
|
|
446 |
* @return a Set of SQLTable.
|
|
|
447 |
*/
|
|
|
448 |
public Set<SQLTable> getTables() {
|
|
|
449 |
return this.getDefaultSchema().getTables();
|
|
|
450 |
}
|
|
|
451 |
|
|
|
452 |
// *** all*
|
|
|
453 |
|
|
|
454 |
public Set<SQLName> getAllTableNames() {
|
|
|
455 |
final Set<SQLName> res = new HashSet<SQLName>();
|
|
|
456 |
for (final SQLTable t : this.getAllTables()) {
|
|
|
457 |
res.add(t.getSQLName(this, false));
|
|
|
458 |
}
|
|
|
459 |
return res;
|
|
|
460 |
}
|
|
|
461 |
|
|
|
462 |
public Set<SQLTable> getAllTables() {
|
|
|
463 |
final Set<SQLTable> res = new HashSet<SQLTable>();
|
|
|
464 |
for (final SQLSchema s : this.getSchemas()) {
|
|
|
465 |
res.addAll(s.getTables());
|
|
|
466 |
}
|
|
|
467 |
return res;
|
|
|
468 |
}
|
|
|
469 |
|
|
|
470 |
// *** schemas
|
|
|
471 |
|
|
|
472 |
@Override
|
61 |
ilm |
473 |
public Map<String, SQLSchema> getChildrenMap() {
|
|
|
474 |
return this.schemas.getImmutable();
|
17 |
ilm |
475 |
}
|
|
|
476 |
|
|
|
477 |
public final Set<SQLSchema> getSchemas() {
|
|
|
478 |
return new HashSet<SQLSchema>(this.schemas.values());
|
|
|
479 |
}
|
|
|
480 |
|
|
|
481 |
public final SQLSchema getSchema(String name) {
|
|
|
482 |
return this.schemas.get(name);
|
|
|
483 |
}
|
|
|
484 |
|
|
|
485 |
/**
|
|
|
486 |
* The current default schema.
|
|
|
487 |
*
|
|
|
488 |
* @return the default schema or <code>null</code>.
|
|
|
489 |
*/
|
|
|
490 |
final SQLSchema getDefaultSchema() {
|
61 |
ilm |
491 |
final Map<String, SQLSchema> children = this.getChildrenMap();
|
|
|
492 |
if (children.size() == 0) {
|
17 |
ilm |
493 |
return null;
|
61 |
ilm |
494 |
} else if (children.size() == 1) {
|
|
|
495 |
return children.values().iterator().next();
|
|
|
496 |
} else if (this.getServer().getSQLSystem().getLevel(DBRoot.class) == HierarchyLevel.SQLSCHEMA) {
|
|
|
497 |
final List<String> path = this.getDBSystemRoot().getRootPath();
|
|
|
498 |
if (path.size() > 0)
|
|
|
499 |
return children.get(path.get(0));
|
|
|
500 |
}
|
|
|
501 |
throw new IllegalStateException();
|
17 |
ilm |
502 |
}
|
|
|
503 |
|
|
|
504 |
private SQLSchema createAndGetSchema(String name) {
|
|
|
505 |
SQLSchema res = this.getSchema(name);
|
|
|
506 |
if (res == null) {
|
|
|
507 |
res = new SQLSchema(this, name);
|
|
|
508 |
this.schemas.put(name, res);
|
|
|
509 |
}
|
|
|
510 |
return res;
|
|
|
511 |
}
|
|
|
512 |
|
|
|
513 |
public final DBRoot guessDBRoot() {
|
|
|
514 |
if (this.getDBRoot() != null)
|
|
|
515 |
return this.getDBRoot();
|
|
|
516 |
else
|
|
|
517 |
return this.getDBSystemRoot().getDefaultRoot();
|
|
|
518 |
}
|
|
|
519 |
|
|
|
520 |
public DatabaseGraph getGraph() {
|
|
|
521 |
if (this.getDBRoot() == null)
|
|
|
522 |
return this.getDBSystemRoot().getGraph();
|
|
|
523 |
else
|
|
|
524 |
return this.getDBRoot().getGraph();
|
|
|
525 |
}
|
|
|
526 |
|
|
|
527 |
/**
|
|
|
528 |
* Vérifie l'intégrité de la base. C'est à dire que les clefs étrangères pointent sur des lignes
|
|
|
529 |
* existantes. Cette méthode renvoie une Map dont les clefs sont les tables présentant des
|
|
|
530 |
* inconsistences. Les valeurs de cette Map sont des List de SQLRow.
|
|
|
531 |
*
|
|
|
532 |
* @return les inconsistences.
|
|
|
533 |
* @see SQLTable#checkIntegrity()
|
|
|
534 |
*/
|
61 |
ilm |
535 |
public Map<SQLTable, List<Tuple3<SQLRow, SQLField, SQLRow>>> checkIntegrity() {
|
|
|
536 |
final Map<SQLTable, List<Tuple3<SQLRow, SQLField, SQLRow>>> inconsistencies = new HashMap<SQLTable, List<Tuple3<SQLRow, SQLField, SQLRow>>>();
|
17 |
ilm |
537 |
for (final SQLTable table : this.getAllTables()) {
|
61 |
ilm |
538 |
List<Tuple3<SQLRow, SQLField, SQLRow>> tableInc = table.checkIntegrity();
|
17 |
ilm |
539 |
if (tableInc.size() > 0)
|
|
|
540 |
inconsistencies.put(table, tableInc);
|
|
|
541 |
}
|
|
|
542 |
return inconsistencies;
|
|
|
543 |
}
|
|
|
544 |
|
|
|
545 |
/**
|
|
|
546 |
* Exécute la requête dans le contexte de cette base et retourne le résultat. Le résultat d'une
|
|
|
547 |
* insertion étant les clefs auto-générées, eg le nouvel ID.
|
|
|
548 |
*
|
|
|
549 |
* @deprecated use getDataSource()
|
|
|
550 |
* @param query le requête à exécuter.
|
|
|
551 |
* @return le résultat de la requête.
|
|
|
552 |
* @see java.sql.Statement#getGeneratedKeys()
|
|
|
553 |
*/
|
|
|
554 |
public ResultSet execute(String query) {
|
|
|
555 |
return this.getDataSource().executeRaw(query);
|
|
|
556 |
}
|
|
|
557 |
|
|
|
558 |
public SQLDataSource getDataSource() {
|
|
|
559 |
return this.getDBSystemRoot().getDataSource();
|
|
|
560 |
}
|
|
|
561 |
|
|
|
562 |
public String toString() {
|
|
|
563 |
return this.getName();
|
|
|
564 |
}
|
|
|
565 |
|
|
|
566 |
// ** metadata
|
|
|
567 |
|
67 |
ilm |
568 |
/**
|
|
|
569 |
* Get a metadata.
|
|
|
570 |
*
|
|
|
571 |
* @param schema the name of the schema.
|
|
|
572 |
* @param name the name of the meta data.
|
|
|
573 |
* @return the requested meta data, can be <code>null</code> (including if
|
|
|
574 |
* {@value SQLSchema#METADATA_TABLENAME} does not exist).
|
|
|
575 |
*/
|
83 |
ilm |
576 |
String getFwkMetadata(String schema, String name) {
|
|
|
577 |
return getFwkMetadata(Collections.singletonList(schema), name).get(schema);
|
|
|
578 |
}
|
|
|
579 |
|
|
|
580 |
private final String getSel(final String schema, final String name, final boolean selSchema) {
|
17 |
ilm |
581 |
final SQLName tableName = new SQLName(this.getName(), schema, SQLSchema.METADATA_TABLENAME);
|
83 |
ilm |
582 |
return "SELECT " + (selSchema ? this.quoteString(schema) + ", " : "") + "\"VALUE\" FROM " + tableName.quote() + " WHERE \"NAME\"= " + this.quoteString(name);
|
|
|
583 |
}
|
|
|
584 |
|
|
|
585 |
private final void exec(final Collection<String> schemas, final String name, final ResultSetHandler rsh) {
|
|
|
586 |
this.getDataSource().execute(CollectionUtils.join(schemas, "\nUNION ALL ", new ITransformer<String, String>() {
|
|
|
587 |
@Override
|
|
|
588 |
public String transformChecked(String schema) {
|
|
|
589 |
// schema name needed since missing values will result in missing rows not
|
|
|
590 |
// null values
|
|
|
591 |
return getSel(schema, name, true);
|
|
|
592 |
}
|
|
|
593 |
}), new IResultSetHandler(rsh, false));
|
|
|
594 |
}
|
|
|
595 |
|
|
|
596 |
Map<String, String> getFwkMetadata(final Collection<String> schemas, final String name) {
|
|
|
597 |
if (schemas.isEmpty())
|
|
|
598 |
return Collections.emptyMap();
|
|
|
599 |
final Map<String, String> res = new LinkedHashMap<String, String>();
|
|
|
600 |
CollectionUtils.fillMap(res, schemas);
|
|
|
601 |
final ResultSetHandler rsh = new ResultSetHandler() {
|
|
|
602 |
@Override
|
|
|
603 |
public Object handle(ResultSet rs) throws SQLException {
|
|
|
604 |
while (rs.next()) {
|
|
|
605 |
res.put(rs.getString(1), rs.getString(2));
|
|
|
606 |
}
|
|
|
607 |
return null;
|
|
|
608 |
}
|
|
|
609 |
};
|
|
|
610 |
try {
|
|
|
611 |
if (this.getDataSource().getTransactionPoint() == null) {
|
|
|
612 |
exec(schemas, name, rsh);
|
|
|
613 |
} else {
|
|
|
614 |
// If already in a transaction, don't risk aborting it if a table doesn't exist.
|
|
|
615 |
// (it's not strictly required for H2 and MySQL, since the transaction is *not*
|
|
|
616 |
// aborted)
|
|
|
617 |
SQLUtils.executeAtomic(this.getDataSource(), new ConnectionHandlerNoSetup<Object, SQLException>() {
|
67 |
ilm |
618 |
@Override
|
83 |
ilm |
619 |
public Object handle(SQLDataSource ds) throws SQLException {
|
|
|
620 |
exec(schemas, name, rsh);
|
|
|
621 |
return null;
|
67 |
ilm |
622 |
}
|
83 |
ilm |
623 |
}, false);
|
67 |
ilm |
624 |
}
|
83 |
ilm |
625 |
} catch (Exception exn) {
|
|
|
626 |
final SQLException sqlExn = SQLUtils.findWithSQLState(exn);
|
|
|
627 |
final boolean tableNotFound = sqlExn != null && (sqlExn.getSQLState().equals("42S02") || sqlExn.getSQLState().equals("42P01"));
|
|
|
628 |
if (!tableNotFound)
|
132 |
ilm |
629 |
throw new IllegalStateException("Not a missing table exception", exn);
|
83 |
ilm |
630 |
|
|
|
631 |
// The following fall back should not currently be needed since the table is created
|
|
|
632 |
// by JDBCStructureSource.getNames(). Even without that most DB should contain the
|
|
|
633 |
// metadata tables.
|
|
|
634 |
|
|
|
635 |
// if only one schema, there's no ambiguity : just return null value
|
|
|
636 |
// otherwise retry with each single schema to find out which ones are missing
|
|
|
637 |
if (schemas.size() > 1) {
|
|
|
638 |
// this won't loop indefinetly since schemas.size() will be 1
|
|
|
639 |
for (final String schema : schemas)
|
|
|
640 |
res.put(schema, this.getFwkMetadata(schema, name));
|
67 |
ilm |
641 |
}
|
|
|
642 |
}
|
83 |
ilm |
643 |
return res;
|
17 |
ilm |
644 |
}
|
|
|
645 |
|
|
|
646 |
public final String getMDName() {
|
|
|
647 |
return this.getServer().getSQLSystem().getMDName(this.getName());
|
|
|
648 |
}
|
|
|
649 |
|
61 |
ilm |
650 |
public synchronized int[] getVersion() throws SQLException {
|
17 |
ilm |
651 |
if (this.dbVersion == null) {
|
|
|
652 |
this.dbVersion = this.getDataSource().useConnection(new ConnectionHandlerNoSetup<int[], SQLException>() {
|
|
|
653 |
@Override
|
|
|
654 |
public int[] handle(SQLDataSource ds) throws SQLException, SQLException {
|
|
|
655 |
final DatabaseMetaData md = ds.getConnection().getMetaData();
|
|
|
656 |
return new int[] { md.getDatabaseMajorVersion(), md.getDatabaseMinorVersion() };
|
|
|
657 |
}
|
|
|
658 |
});
|
|
|
659 |
}
|
|
|
660 |
return this.dbVersion;
|
|
|
661 |
}
|
|
|
662 |
|
|
|
663 |
// ** files
|
|
|
664 |
|
67 |
ilm |
665 |
static final String FILENAME = "structure.xml";
|
17 |
ilm |
666 |
|
|
|
667 |
static final boolean isSaved(final SQLServer s, final String base, final String schema) {
|
|
|
668 |
return s.getFileCache().getChild(base, schema).getFile(SQLBase.FILENAME).exists();
|
|
|
669 |
}
|
|
|
670 |
|
|
|
671 |
/**
|
67 |
ilm |
672 |
* Where xml dumps are saved, always <code>null</code> if {@link DBSystemRoot#useCache()} is
|
17 |
ilm |
673 |
* <code>false</code>.
|
|
|
674 |
*
|
|
|
675 |
* @return the directory of xmls dumps, <code>null</code> if it can't be found.
|
|
|
676 |
*/
|
|
|
677 |
private final DBItemFileCache getFileCache() {
|
67 |
ilm |
678 |
final boolean useXML = this.getDBSystemRoot().useCache();
|
17 |
ilm |
679 |
final DBFileCache fileCache = this.getServer().getFileCache();
|
|
|
680 |
if (!useXML || fileCache == null)
|
|
|
681 |
return null;
|
|
|
682 |
else {
|
|
|
683 |
return fileCache.getChild(this.getName());
|
|
|
684 |
}
|
|
|
685 |
}
|
|
|
686 |
|
67 |
ilm |
687 |
private final DBItemFileCache getSchemaFileCache(String schema) {
|
17 |
ilm |
688 |
final DBItemFileCache item = this.getFileCache();
|
|
|
689 |
if (item == null)
|
|
|
690 |
return null;
|
67 |
ilm |
691 |
return item.getChild(schema);
|
17 |
ilm |
692 |
}
|
|
|
693 |
|
67 |
ilm |
694 |
final List<DBItemFileCache> getSavedShemaCaches() {
|
|
|
695 |
return this.getSavedCaches(true);
|
|
|
696 |
}
|
|
|
697 |
|
17 |
ilm |
698 |
private final List<DBItemFileCache> getSavedCaches(boolean withStruct) {
|
|
|
699 |
final DBItemFileCache item = this.getFileCache();
|
|
|
700 |
if (item == null)
|
|
|
701 |
return Collections.emptyList();
|
|
|
702 |
else {
|
|
|
703 |
return item.getSavedDesc(SQLSchema.class, withStruct ? FILENAME : null);
|
|
|
704 |
}
|
|
|
705 |
}
|
|
|
706 |
|
|
|
707 |
final boolean isSaved(String schema) {
|
|
|
708 |
return isSaved(this.getServer(), this.getName(), schema);
|
|
|
709 |
}
|
|
|
710 |
|
|
|
711 |
/**
|
|
|
712 |
* Deletes all files containing information about this base's structure.
|
|
|
713 |
*/
|
|
|
714 |
public void deleteStructureFiles() {
|
|
|
715 |
for (final DBItemFileCache f : this.getSavedCaches(true)) {
|
|
|
716 |
f.getFile(FILENAME).delete();
|
|
|
717 |
}
|
|
|
718 |
}
|
|
|
719 |
|
80 |
ilm |
720 |
boolean save(final String schemaName) {
|
67 |
ilm |
721 |
final DBItemFileCache schemaFileCache = this.getSchemaFileCache(schemaName);
|
|
|
722 |
if (schemaFileCache == null) {
|
17 |
ilm |
723 |
return false;
|
67 |
ilm |
724 |
} else {
|
|
|
725 |
final File schemaFile = schemaFileCache.getFile(FILENAME);
|
80 |
ilm |
726 |
return AccessController.doPrivileged(new PrivilegedAction<Boolean>() {
|
|
|
727 |
@Override
|
|
|
728 |
public Boolean run() {
|
|
|
729 |
Writer pWriter = null;
|
|
|
730 |
try {
|
|
|
731 |
final String schema = getSchema(schemaName).toXML();
|
|
|
732 |
if (schema == null)
|
|
|
733 |
return false;
|
|
|
734 |
FileUtils.mkdir_p(schemaFile.getParentFile());
|
|
|
735 |
// Might save garbage if two threads open the same file
|
|
|
736 |
synchronized (this) {
|
|
|
737 |
pWriter = FileUtils.createXMLWriter(schemaFile);
|
|
|
738 |
pWriter.write("<root codecVersion=\"" + XMLStructureSource.version + "\" >\n" + schema + "\n</root>\n");
|
|
|
739 |
}
|
17 |
ilm |
740 |
|
80 |
ilm |
741 |
return true;
|
|
|
742 |
} catch (Exception e) {
|
|
|
743 |
Log.get().log(Level.WARNING, "unable to save files in " + schemaFile, e);
|
|
|
744 |
return false;
|
|
|
745 |
} finally {
|
|
|
746 |
if (pWriter != null) {
|
|
|
747 |
try {
|
|
|
748 |
pWriter.close();
|
|
|
749 |
} catch (IOException e) {
|
|
|
750 |
e.printStackTrace();
|
|
|
751 |
}
|
|
|
752 |
}
|
67 |
ilm |
753 |
}
|
|
|
754 |
}
|
80 |
ilm |
755 |
});
|
67 |
ilm |
756 |
}
|
17 |
ilm |
757 |
}
|
|
|
758 |
|
|
|
759 |
// *** quoting
|
|
|
760 |
|
|
|
761 |
// * quote
|
|
|
762 |
|
|
|
763 |
/**
|
|
|
764 |
* Quote %-escaped parameters. %% : %, %s : {@link #quoteString(String)}, %i : an identifier
|
|
|
765 |
* string, if it's a SQLName calls {@link SQLName#quote()} else {@link #quoteIdentifier(String)}
|
|
|
766 |
* , %f or %n : respectively fullName and name of an SQLIdentifier of a DBStructureItem.
|
|
|
767 |
*
|
|
|
768 |
* @param pattern a string with %, eg "SELECT * FROM %n where %f like '%%a%%'".
|
|
|
769 |
* @param params the parameters, eg [ /TENSION/, |TENSION.LABEL| ].
|
|
|
770 |
* @return pattern with % replaced, eg SELECT * FROM "TENSION" where "TENSION.LABEL" like '%a%'.
|
|
|
771 |
*/
|
|
|
772 |
public final String quote(final String pattern, Object... params) {
|
182 |
ilm |
773 |
return quote(this.getDBSystemRoot().getSyntax(), pattern, params);
|
17 |
ilm |
774 |
}
|
|
|
775 |
|
|
|
776 |
static private final Pattern percent = Pattern.compile("%.");
|
|
|
777 |
|
182 |
ilm |
778 |
final static String quote(final SQLSyntax s, final String pattern, Object... params) {
|
|
|
779 |
Objects.requireNonNull(s, "Missing syntax");
|
17 |
ilm |
780 |
final Matcher m = percent.matcher(pattern);
|
|
|
781 |
final StringBuffer sb = new StringBuffer();
|
|
|
782 |
int i = 0;
|
|
|
783 |
int lastAppendPosition = 0;
|
|
|
784 |
while (m.find()) {
|
|
|
785 |
final String replacement;
|
|
|
786 |
final char modifier = m.group().charAt(m.group().length() - 1);
|
|
|
787 |
if (modifier == '%') {
|
|
|
788 |
replacement = "%";
|
|
|
789 |
} else {
|
|
|
790 |
final Object param = params[i++];
|
|
|
791 |
if (modifier == 's') {
|
182 |
ilm |
792 |
replacement = s.quoteString(param.toString());
|
17 |
ilm |
793 |
} else if (modifier == 'i') {
|
|
|
794 |
if (param instanceof SQLName)
|
|
|
795 |
replacement = ((SQLName) param).quote();
|
|
|
796 |
else
|
|
|
797 |
replacement = quoteIdentifier(param.toString());
|
|
|
798 |
} else {
|
80 |
ilm |
799 |
final SQLIdentifier ident = (SQLIdentifier) ((DBStructureItem<?>) param).getJDBC();
|
17 |
ilm |
800 |
if (modifier == 'f') {
|
|
|
801 |
replacement = ident.getSQLName().quote();
|
|
|
802 |
} else if (modifier == 'n')
|
|
|
803 |
replacement = quoteIdentifier(ident.getName());
|
|
|
804 |
else
|
|
|
805 |
throw new IllegalArgumentException("unknown modifier: " + modifier);
|
|
|
806 |
}
|
|
|
807 |
}
|
|
|
808 |
|
|
|
809 |
// do NOT use appendReplacement() (and appendTail()) since it parses \ and $
|
|
|
810 |
// Append the intervening text
|
|
|
811 |
sb.append(pattern.subSequence(lastAppendPosition, m.start()));
|
|
|
812 |
// Append the match substitution
|
|
|
813 |
sb.append(replacement);
|
|
|
814 |
lastAppendPosition = m.end();
|
|
|
815 |
}
|
|
|
816 |
sb.append(pattern.substring(lastAppendPosition));
|
|
|
817 |
return sb.toString();
|
|
|
818 |
}
|
|
|
819 |
|
|
|
820 |
// * quoteString
|
|
|
821 |
|
|
|
822 |
/**
|
|
|
823 |
* Quote an sql string specifically for this base.
|
|
|
824 |
*
|
|
|
825 |
* @param s an arbitrary string, eg "salut\ l'ami".
|
|
|
826 |
* @return the quoted form, eg "'salut\\ l''ami'".
|
|
|
827 |
* @see #quoteStringStd(String)
|
|
|
828 |
*/
|
|
|
829 |
public String quoteString(String s) {
|
142 |
ilm |
830 |
return SQLSyntax.get(this).quoteString(s);
|
17 |
ilm |
831 |
}
|
|
|
832 |
|
67 |
ilm |
833 |
static private final Pattern singleQuote = Pattern.compile("'", Pattern.LITERAL);
|
142 |
ilm |
834 |
static public final Pattern quotedPatrn = Pattern.compile("'(('')|[^'])*'");
|
67 |
ilm |
835 |
static private final Pattern twoSingleQuote = Pattern.compile("''", Pattern.LITERAL);
|
17 |
ilm |
836 |
|
|
|
837 |
/**
|
|
|
838 |
* Quote an sql string the standard way. See section 4.1.2.1. String Constants of postgresql
|
|
|
839 |
* documentation.
|
|
|
840 |
*
|
|
|
841 |
* @param s an arbitrary string, eg "salut\ l'ami".
|
|
|
842 |
* @return the quoted form, eg "'salut\ l''ami'".
|
|
|
843 |
*/
|
|
|
844 |
public final static String quoteStringStd(String s) {
|
83 |
ilm |
845 |
return s == null ? "NULL" : "'" + singleQuote.matcher(s).replaceAll("''") + "'";
|
17 |
ilm |
846 |
}
|
|
|
847 |
|
67 |
ilm |
848 |
/**
|
|
|
849 |
* Unquote an SQL string the standard way.
|
|
|
850 |
* <p>
|
|
|
851 |
* NOTE : There's no unquoteString() instance method since it can be affected by session
|
|
|
852 |
* parameters. So to be correct the method should execute a request each time to find out these
|
|
|
853 |
* values. But if it did that, it might as well execute <code>"SELECT ?"</code> with the string
|
|
|
854 |
* (and <b>not</b> <code>executeScalar("SELECT " + s)</code> to avoid SQL injection).
|
|
|
855 |
* </p>
|
|
|
856 |
*
|
|
|
857 |
* @param s an arbitrary SQL string, e.g. 'salu\t l''ami'.
|
|
|
858 |
* @return the java string, e.g. "salu\\t l'ami".
|
|
|
859 |
* @see #quoteStringStd(String)
|
|
|
860 |
*/
|
|
|
861 |
public final static String unquoteStringStd(String s) {
|
142 |
ilm |
862 |
if (!quotedPatrn.matcher(s).matches())
|
67 |
ilm |
863 |
throw new IllegalArgumentException("Invalid quoted string " + s);
|
|
|
864 |
return twoSingleQuote.matcher(s.substring(1, s.length() - 1)).replaceAll("'");
|
|
|
865 |
}
|
|
|
866 |
|
17 |
ilm |
867 |
// * quoteIdentifier
|
|
|
868 |
|
|
|
869 |
static private final Pattern doubleQuote = Pattern.compile("\"");
|
|
|
870 |
|
|
|
871 |
/**
|
|
|
872 |
* Quote a sql identifier to prevent it from being folded and allow any character.
|
|
|
873 |
*
|
|
|
874 |
* @param identifier a SQL identifier, eg 'My"Table'.
|
|
|
875 |
* @return the quoted form, eg '"My""Table"'.
|
|
|
876 |
*/
|
|
|
877 |
public static final String quoteIdentifier(String identifier) {
|
|
|
878 |
return '"' + doubleQuote.matcher(identifier).replaceAll("\"\"") + '"';
|
|
|
879 |
}
|
|
|
880 |
}
|