dd134f37cb183326eea08b67aeadb1e9003200a4
[cdmlib.git] / cdmlib-persistence / src / main / java / eu / etaxonomy / cdm / database / update / TableCreator.java
1 /**
2 * Copyright (C) 2009 EDIT
3 * European Distributed Institute of Taxonomy
4 * http://www.e-taxonomy.eu
5 *
6 * The contents of this file are subject to the Mozilla Public License Version 1.1
7 * See LICENSE.TXT at the top of this package for the full license terms.
8 */
9 package eu.etaxonomy.cdm.database.update;
10
11 import java.util.ArrayList;
12 import java.util.Arrays;
13 import java.util.List;
14
15 import org.apache.commons.lang.StringUtils;
16 import org.apache.logging.log4j.LogManager;import org.apache.logging.log4j.Logger;
17
18 import eu.etaxonomy.cdm.common.CdmUtils;
19 import eu.etaxonomy.cdm.common.monitor.IProgressMonitor;
20 import eu.etaxonomy.cdm.database.DatabaseTypeEnum;
21 import eu.etaxonomy.cdm.database.ICdmDataSource;
22
23 /**
24 * @author a.mueller
25 * @since 16.09.2010
26 */
27 public class TableCreator extends AuditedSchemaUpdaterStepBase {
28 private static final Logger logger = LogManager.getLogger(TableCreator.class);
29
30 private static final boolean IS_LIST = true;
31 private static final boolean IS_1_TO_M = true;
32 private static final boolean IS_M_TO_M = false;
33
34 private final List<String> columnNames;
35 private final List<String> columnTypes;
36 private final List<Object> defaultValues;
37 private final List<Boolean> isNotNull;
38 private final List<String> referencedTables;
39 private final boolean includeCdmBaseAttributes;
40 private final boolean includeIdentifiableEntity;
41 private final boolean includeAnnotatableEntity;
42 private final boolean includeSingleSourcedEntity;
43 private boolean includeEventBase;
44 private final boolean excludeVersionableAttributes;
45 protected List<ColumnAdder> columnAdders = new ArrayList<>();
46 protected List<ISchemaUpdaterStep> mnTablesStepList = new ArrayList<>();
47 private String primaryKeyParams;
48 private String primaryKeyParams_AUD;
49 private String uniqueParams;
50 private String uniqueParams_AUD;
51
52
53 public static final TableCreator NewInstance(List<ISchemaUpdaterStep> stepList, String stepName, String tableName, List<String> columnNames, List<String> columnTypes, boolean includeAudTable, boolean includeCdmBaseAttributes){
54 return new TableCreator(stepList, stepName, tableName, columnNames, columnTypes, null, null, null, includeAudTable, includeCdmBaseAttributes, false, false, false, false);
55 }
56
57 public static final TableCreator NewInstance(List<ISchemaUpdaterStep> stepList, String stepName, String tableName, String[] columnNames, String[] columnTypes, String[] referencedTables, boolean includeAudTable, boolean includeCdmBaseAttributes){
58 return new TableCreator(stepList, stepName, tableName, Arrays.asList(columnNames), Arrays.asList(columnTypes), null, null, Arrays.asList(referencedTables), includeAudTable, includeCdmBaseAttributes, false, false, false, false);
59 }
60
61 public static final TableCreator NewAuditedCdmBaseInstance(List<ISchemaUpdaterStep> stepList, String stepName, String tableName, String[] columnNames, String[] columnTypes, String[] referencedTables){
62 return new TableCreator(stepList, stepName, tableName, Arrays.asList(columnNames), Arrays.asList(columnTypes), null, null, Arrays.asList(referencedTables), true, true, false, false, true, false);
63 }
64
65 public static final TableCreator NewNonVersionableInstance(List<ISchemaUpdaterStep> stepList, String stepName, String tableName, String[] columnNames, String[] columnTypes, String[] referencedTables){
66 return new TableCreator(stepList, stepName, tableName, Arrays.asList(columnNames), Arrays.asList(columnTypes), null, null, Arrays.asList(referencedTables), false, true, false, false, true, false);
67 }
68
69 public static final TableCreator NewVersionableInstance(List<ISchemaUpdaterStep> stepList, String stepName, String tableName, String[] columnNames, String[] columnTypes, String[] referencedTables, boolean includeAudTable){
70 return new TableCreator(stepList, stepName, tableName, Arrays.asList(columnNames), Arrays.asList(columnTypes), null, null, Arrays.asList(referencedTables), includeAudTable, true, false, false, false, false);
71 }
72
73 public static final TableCreator NewAnnotatableInstance(List<ISchemaUpdaterStep> stepList, String stepName, String tableName, String[] columnNames, String[] columnTypes, String[] referencedTables, boolean includeAudTable){
74 return new TableCreator(stepList, stepName, tableName, Arrays.asList(columnNames), Arrays.asList(columnTypes), null, null, Arrays.asList(referencedTables), includeAudTable, true, true, false, false, false);
75 }
76
77 public static final TableCreator NewSingleSourcedInstance(List<ISchemaUpdaterStep> stepList, String stepName, String tableName, String[] columnNames, String[] columnTypes, String[] referencedTables, boolean includeAudTable){
78 return new TableCreator(stepList, stepName, tableName, Arrays.asList(columnNames), Arrays.asList(columnTypes), null, null, Arrays.asList(referencedTables), includeAudTable, true, true, false, false, true);
79 }
80
81 public static final TableCreator NewEventInstance(List<ISchemaUpdaterStep> stepList, String stepName, String tableName, String[] columnNames, String[] columnTypes, String[] referencedTables, boolean includeAudTable){
82 TableCreator result = new TableCreator(stepList, stepName, tableName, Arrays.asList(columnNames), Arrays.asList(columnTypes), null, null, Arrays.asList(referencedTables), includeAudTable, true, true, false, false, false);
83 result.includeEventBase = true;
84 return result;
85 }
86
87 public static final TableCreator NewIdentifiableInstance(List<ISchemaUpdaterStep> stepList, String stepName, String tableName, String[] columnNames, String[] columnTypes, String[] referencedTables, boolean includeAudTable){
88 return new TableCreator(stepList, stepName, tableName, Arrays.asList(columnNames), Arrays.asList(columnTypes), null, null, Arrays.asList(referencedTables), includeAudTable, true, true, true, false, false);
89 }
90
91 protected TableCreator(List<ISchemaUpdaterStep> stepList, String stepName, String tableName, List<String> columnNames, List<String> columnTypes, List<Object> defaultValues, List<Boolean> isNotNull, List<String> referencedTables,
92 boolean includeAudTable, boolean includeCdmBaseAttributes, boolean includeAnnotatableEntity, boolean includeIdentifiableEntity, boolean excludeVersionableAttributes, boolean includeSingleSourcedEntity) {
93 super(stepList, stepName, tableName, includeAudTable);
94 this.columnNames = columnNames;
95 this.columnTypes = columnTypes;
96 this.defaultValues = defaultValues;
97 this.isNotNull = isNotNull;
98 this.referencedTables = referencedTables;
99 this.includeCdmBaseAttributes = includeCdmBaseAttributes;
100 this.includeAnnotatableEntity = includeAnnotatableEntity;
101 this.includeIdentifiableEntity = includeIdentifiableEntity;
102 this.excludeVersionableAttributes = excludeVersionableAttributes;
103 this.includeSingleSourcedEntity = includeSingleSourcedEntity;
104 makeColumnAdders();
105 makeMnTables(mnTablesStepList, this.tableName, this.includeAnnotatableEntity, this.includeIdentifiableEntity);
106 }
107
108
109 @Override
110 public List<ISchemaUpdaterStep> getInnerSteps() {
111 return mnTablesStepList;
112 }
113
114 /**
115 * Fills the {@link #columnAdders} list.
116 */
117 private void makeColumnAdders() {
118 if (columnNames.size() != columnTypes.size()){
119 throw new RuntimeException ("ColumnNames and columnTypes must be of same size. Step: " + getStepName());
120 }
121
122 try {
123 for (int i = 0; i < columnNames.size(); i++){
124 boolean isNotNull = this.isNotNull == null ? false : this.isNotNull.get(i);
125 if ("integer".equals(columnTypes.get(i)) || "int".equals(columnTypes.get(i))){
126 String referencedTable = (this.referencedTables == null) ? null : this.referencedTables.get(i);
127 ColumnAdder.NewIntegerInstance(columnAdders, this.getStepName(), this.tableName, this.columnNames.get(i), includeAudTable, isNotNull, referencedTable);
128 }else if ("boolean".equals(columnTypes.get(i)) || "bit".equals(columnTypes.get(i))){
129 String defaultValue = this.defaultValues == null ? null : this.defaultValues.get(i).toString();
130 ColumnAdder.NewBooleanInstance(columnAdders, getStepName(), this.tableName, this.columnNames.get(i), includeAudTable, Boolean.valueOf(defaultValue));
131 }else if (columnTypes.get(i).startsWith("string")){
132 Integer length = Integer.valueOf(columnTypes.get(i).substring("string_".length()));
133 ColumnAdder.NewStringInstance(columnAdders, getStepName(), this.tableName, this.columnNames.get(i), length, includeAudTable);
134 }else if (columnTypes.get(i).startsWith("clob")){
135 ColumnAdder.NewClobInstance(columnAdders, this.getStepName(), this.tableName, this.columnNames.get(i), includeAudTable);
136 }else if ("tinyint".equals(columnTypes.get(i)) ){
137 ColumnAdder.NewTinyIntegerInstance(columnAdders, this.getStepName(), this.tableName, this.columnNames.get(i), includeAudTable, isNotNull);
138 }else if ("datetime".equals(columnTypes.get(i)) ){
139 ColumnAdder.NewDateTimeInstance(columnAdders, this.getStepName(), this.tableName, this.columnNames.get(i), includeAudTable, isNotNull);
140 }else if ("double".equals(columnTypes.get(i)) ){
141 ColumnAdder.NewDoubleInstance(columnAdders, this.getStepName(), this.tableName, this.columnNames.get(i), includeAudTable, isNotNull);
142 }else{
143 throw new RuntimeException("Column type " + columnTypes.get(i) + " not yet supported");
144 }
145 }
146 } catch (Exception e) {
147 throw new RuntimeException(e);
148 }
149 }
150
151 /**
152 * fills the mnTablesStepList
153 */
154 public static void makeMnTables(List<ISchemaUpdaterStep> mnTablesStepList, String tableName, boolean includeAnnotatable, boolean includeIdentifiable) {
155
156 String stepName;
157
158 if (includeAnnotatable){
159 //annotations
160 stepName= "Add @tableName annotations";
161 stepName = stepName.replace("@tableName", tableName);
162 MnTableCreator.NewMnInstance(mnTablesStepList, stepName, tableName, "Annotation", SchemaUpdaterBase.INCLUDE_AUDIT, !IS_LIST, IS_1_TO_M);
163
164 //marker
165 stepName= "Add @tableName marker";
166 stepName = stepName.replace("@tableName", tableName);
167 MnTableCreator.NewMnInstance(mnTablesStepList, stepName, tableName, "Marker", SchemaUpdaterBase.INCLUDE_AUDIT, !IS_LIST, IS_1_TO_M);
168 }
169
170 if (includeIdentifiable){
171
172 //credits
173 stepName= "Add @tableName credits";
174 stepName = stepName.replace("@tableName", tableName);
175 MnTableCreator.NewMnInstance(mnTablesStepList, stepName, tableName, null, "Credit", null, null, SchemaUpdaterBase.INCLUDE_AUDIT, IS_LIST, IS_1_TO_M);
176
177 //identifier
178 stepName= "Add @tableName identifiers";
179 stepName = stepName.replace("@tableName", tableName);
180 MnTableCreator.NewMnInstance(mnTablesStepList, stepName, tableName, null, "Identifier", null, null, SchemaUpdaterBase.INCLUDE_AUDIT, IS_LIST, IS_1_TO_M);
181
182 //extensions
183 stepName= "Add @tableName extensions";
184 stepName = stepName.replace("@tableName", tableName);
185 MnTableCreator.NewMnInstance(mnTablesStepList, stepName, tableName, "Extension", SchemaUpdaterBase.INCLUDE_AUDIT, !IS_LIST, IS_1_TO_M);
186
187 //OriginalSourceBase
188 stepName= "Add @tableName sources";
189 stepName = stepName.replace("@tableName", tableName);
190 MnTableCreator.NewMnInstance(mnTablesStepList, stepName, tableName, null, "OriginalSourceBase", null, "sources", SchemaUpdaterBase.INCLUDE_AUDIT, ! IS_LIST, IS_1_TO_M);
191
192 //Rights
193 stepName= "Add @tableName rights";
194 stepName = stepName.replace("@tableName", tableName);
195 MnTableCreator.NewMnInstance(mnTablesStepList, stepName, tableName, null, "RightsInfo", null, "rights", SchemaUpdaterBase.INCLUDE_AUDIT, !IS_LIST, IS_M_TO_M);
196 }
197
198 }
199
200
201 @Override
202 protected void invokeOnTable(String tableName, ICdmDataSource datasource,
203 IProgressMonitor monitor, CaseType caseType, SchemaUpdateResult result) {
204 try {
205 //CREATE
206 String updateQuery = "CREATE TABLE @tableName (";
207 //AUDIT
208 if (isAuditing){
209 updateQuery += " REV integer not null, revtype " + Datatype.TINYINTEGER.format(datasource, null) + ", ";
210 }
211 //CdmBase
212 if (includeCdmBaseAttributes){
213 String uuidNull = isAuditing? "": "NOT NULL";
214 updateQuery += " id integer NOT NULL,"
215 + " created " + Datatype.DATETIME.format(datasource, null) + ", "
216 + " uuid varchar(36) "+uuidNull+","
217 + (excludeVersionableAttributes? "" : " updated " + Datatype.DATETIME.format(datasource, null) + ", ")
218 + " createdby_id integer,"
219 + (excludeVersionableAttributes ? "" : " updatedby_id integer, ");
220 }
221 //EventBase
222 if (this.includeEventBase){
223 updateQuery += "timeperiod_start varchar(255), timeperiod_end varchar(255), timeperiod_freetext varchar(255), actor_id int, description varchar(255),";
224 }
225 //Identifiable
226 if (this.includeIdentifiableEntity){
227 updateQuery += "lsid_authority varchar(255), lsid_lsid varchar(255), lsid_namespace varchar(255), lsid_object varchar(255), lsid_revision varchar(255), protectedtitlecache bit not null, titleCache varchar(255),";
228 }
229 if (this.includeSingleSourcedEntity){
230 updateQuery += "source_id integer, ";
231 }
232 //specific columns
233 updateQuery += getColumnsSql(tableName, datasource, monitor);
234
235 //primary and unique keys
236 String primaryKeySql = primaryKey(isAuditing)==null ? "" : "primary key (" + primaryKey(isAuditing) + "),";
237 String uniqueSql = unique(isAuditing)== null ? "" : "unique(" + unique(isAuditing) + "),";
238 updateQuery += primaryKeySql + uniqueSql;
239
240 //finalize
241 updateQuery = StringUtils.chomp(updateQuery.trim(), ",") + ")";
242
243 //replace
244 updateQuery = updateQuery.replace("@tableName", tableName);
245
246 //append datasource specific string
247 updateQuery += datasource.getDatabaseType().getHibernateDialect().getTableTypeString();
248 logger.debug("UPDATE Query: " + updateQuery);
249
250 //execute
251 datasource.executeUpdate(updateQuery);
252
253 //Foreign Keys
254 createForeignKeys(tableName, isAuditing, datasource, monitor, caseType, result);
255
256 return;
257 } catch (Exception e) {
258 monitor.warning(e.getMessage(), e);
259 logger.error(e);
260 result.addException(e, e.getMessage(), "TableCreator.invokeOnTable");
261 return;
262 }
263 }
264
265
266 /**
267 * Returns the sql part for the {@link #columnAdders} columns.
268 * This is done by reusing the same method in the ColumnAdder class and removing all the prefixes like 'ADD COLUMN'
269 */
270 private String getColumnsSql(String tableName, ICdmDataSource datasource, IProgressMonitor monitor) throws DatabaseTypeNotSupportedException {
271 String result = "";
272 for (ColumnAdder adder : this.columnAdders){
273 String singleAdderSQL = adder.getUpdateQueryString(tableName, datasource, monitor) + ", ";
274
275 String[] split = singleAdderSQL.split(ColumnAdder.getAddColumnSeperator(datasource));
276 result += split[1];
277 }
278 return result;
279 }
280
281
282 private void createForeignKeys(String tableName, boolean isAudit, ICdmDataSource datasource,
283 IProgressMonitor monitor, CaseType caseType, SchemaUpdateResult result) {
284 if (includeCdmBaseAttributes){
285 //updatedBy
286 if (! this.excludeVersionableAttributes){
287 String attribute = "updatedby";
288 String referencedTable = "UserAccount";
289 makeForeignKey(tableName, datasource, monitor, attribute,
290 referencedTable, caseType, result);
291 }
292
293 //createdBy
294 String attribute = "createdby";
295 String referencedTable = "UserAccount";
296 makeForeignKey(tableName, datasource, monitor, attribute,
297 referencedTable, caseType, result);
298
299 }
300 if (isAudit){
301 //REV
302 String attribute = "REV";
303 String referencedTable = "AuditEvent";
304 makeForeignKey(tableName, datasource, monitor, attribute,
305 referencedTable, caseType, result);
306 }
307 if (this.includeEventBase){
308 //actor
309 String attribute = "actor_id";
310 String referencedTable = "AgentBase";
311 makeForeignKey(tableName, datasource, monitor, attribute,
312 referencedTable, caseType, result);
313 }
314 if (this.includeSingleSourcedEntity){
315 //source
316 String attribute = "source_id";
317 String referencedTable = "OriginalSourceBase";
318 makeForeignKey(tableName, datasource, monitor, attribute,
319 referencedTable, caseType, result);
320 }
321
322 for (ColumnAdder adder : this.columnAdders){
323 if (adder.getReferencedTable() != null){
324 makeForeignKey(tableName, datasource, monitor, adder.getNewColumnName(),
325 adder.getReferencedTable(), caseType, result);
326 }
327 }
328 return;
329 }
330
331
332 public static void makeForeignKey(String tableName, ICdmDataSource datasource,
333 IProgressMonitor monitor, String attribute, String referencedTable, CaseType caseType,
334 SchemaUpdateResult result) {
335
336 referencedTable = caseType.transformTo(referencedTable);
337
338 String idSuffix = "_id";
339 if (isRevAttribute(attribute) || attribute.endsWith(idSuffix)){
340 idSuffix = "";
341 }
342 String columnName = attribute + idSuffix;
343
344 if (supportsForeignKeys(datasource, monitor, tableName, referencedTable)){
345 String index = "FK@tableName_@attribute";
346 index = index.replace("@tableName", tableName);
347 index = index.replace("@attribute", attribute);
348
349
350 //OLD - don't remember why we used ADD INDEX here
351 // String updateQuery = "ALTER TABLE @tableName ADD INDEX @index (@attribute), ADD FOREIGN KEY (@attribute) REFERENCES @referencedTable (@id)";
352 String updateQuery = "ALTER TABLE @tableName ADD @constraintName FOREIGN KEY (@attribute) REFERENCES @referencedTable (@id)";
353 updateQuery = updateQuery.replace("@tableName", tableName);
354 // updateQuery = updateQuery.replace("@index", index);
355 updateQuery = updateQuery.replace("@attribute", columnName);
356 updateQuery = updateQuery.replace("@referencedTable", referencedTable);
357 if (datasource.getDatabaseType().equals(DatabaseTypeEnum.MySQL)){
358 updateQuery = updateQuery.replace("@constraintName", "CONSTRAINT " + index);
359 }else{
360 updateQuery = updateQuery.replace("@constraintName", ""); //H2 does not support "CONSTRAINT", didn't check for others
361 }
362
363 if (isRevAttribute(attribute)){
364 updateQuery = updateQuery.replace("@id", "revisionnumber");
365 }else{
366 updateQuery = updateQuery.replace("@id", "id");
367 }
368 logger.debug(updateQuery);
369 try {
370 datasource.executeUpdate(updateQuery);
371 } catch (Exception e) {
372 String message = "Problem when creating Foreign Key for " + tableName +"." + attribute +": " + e.getMessage();
373 monitor.warning(message);
374 logger.warn(message, e);
375 result.addWarning(message);
376 return; //we do not interrupt update if only foreign key generation did not work
377 }
378 return;
379 }else{
380 //create only index
381 IndexAdder indexAdder = IndexAdder.NewIntegerInstance(null, "Add index instead of Foreign Key", tableName, columnName);
382 try {
383 indexAdder.invoke(datasource, monitor, caseType, result);
384 } catch (Exception e) {
385 String message = "Problem when creating index instead of Foreign Key for " + tableName +"." + columnName +": " + e.getMessage();
386 monitor.warning(message);
387 logger.warn(message, e);
388 result.addWarning(message);
389 return; //we do not interrupt update if only index generation did not work
390 }
391 return;
392 }
393 }
394
395 /**
396 * Determines if the tables and the database support foreign keys. If determination is not possible true is returned as default.
397 */
398 private static boolean supportsForeignKeys(ICdmDataSource datasource, IProgressMonitor monitor, String tableName, String referencedTable) {
399 boolean result = true;
400 if (! datasource.getDatabaseType().equals(DatabaseTypeEnum.MySQL)){
401 return true;
402 }else{
403 try {
404 String myIsamTables = "";
405 String format = "SELECT ENGINE FROM information_schema.TABLES where TABLE_SCHEMA = '%s' AND TABLE_NAME = '%s'";
406 String sql = String.format(format, datasource.getDatabase(), tableName);
407 String engine = (String)datasource.getSingleValue(sql);
408 if (engine.equals("MyISAM")){
409 result = false;
410 myIsamTables = CdmUtils.concat(",", myIsamTables, tableName);
411 }
412 sql = String.format(format, datasource.getDatabase(), referencedTable);
413 engine = (String)datasource.getSingleValue(sql);
414 if (engine.equals("MyISAM")){
415 result = false;
416 myIsamTables = CdmUtils.concat(",", myIsamTables, referencedTable);
417 }
418 if (result == false){
419 String message = "Tables (%s) use MyISAM engine. MyISAM does not support foreign keys.";
420 message = String.format(message, myIsamTables);
421 monitor.warning(message);
422 }
423 return result;
424 } catch (Exception e) {
425 String message = "Problems to determine table engine for MySQL.";
426 monitor.warning(message);
427 return true; //default
428 }
429 }
430 }
431
432 private static boolean isRevAttribute(String attribute) {
433 return "REV".equalsIgnoreCase(attribute);
434 }
435
436
437 /**
438 * Constructs the primary key creation string
439 * @param isAudit
440 * @return
441 */
442 protected String primaryKey(boolean isAudit){
443 String result = null;
444 if (! isAudit && this.primaryKeyParams != null){
445 return this.primaryKeyParams;
446 }else if (isAudit && this.primaryKeyParams_AUD != null){
447 return this.primaryKeyParams_AUD;
448 }
449
450 if (includeCdmBaseAttributes || ! includeCdmBaseAttributes){ //TODO how to handle not CDMBase includes => via this.primaryKeyParams
451 if (! isAudit){
452 result = "id";
453 }else{
454 result = "id, REV";
455 }
456 }
457 return result;
458 }
459
460 /**
461 * Constructs the unique key creation string
462 * @param isAudit
463 * @return
464 */
465 protected String unique(boolean isAudit){
466 if (! isAudit){
467 if (this.uniqueParams != null){
468 return this.uniqueParams;
469 }
470 if (includeCdmBaseAttributes){
471 return "uuid"; //TODO how to handle not CDMBase includes
472 }
473 return null;
474 }else{
475 if (this.uniqueParams_AUD != null){
476 return this.uniqueParams_AUD;
477 }
478 return null;
479 }
480 }
481
482 public TableCreator setPrimaryKeyParams(String primaryKeyParams, String primaryKeyParams_AUD) {
483 this.primaryKeyParams = primaryKeyParams;
484 this.primaryKeyParams_AUD = primaryKeyParams_AUD;
485 return this;
486 }
487
488 public TableCreator setUniqueParams(String uniqueParams, String uniqueParams_AUD) {
489 this.uniqueParams = uniqueParams;
490 this.uniqueParams_AUD = uniqueParams_AUD;
491 return this;
492 }
493 }