001/* 002 * Copyright 2010 Anyware Services 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.ametys.plugins.forms.table; 017 018import java.io.IOException; 019import java.net.URISyntaxException; 020import java.sql.Connection; 021import java.sql.DatabaseMetaData; 022import java.sql.PreparedStatement; 023import java.sql.ResultSet; 024import java.sql.SQLException; 025import java.sql.Timestamp; 026import java.sql.Types; 027import java.util.ArrayList; 028import java.util.Collections; 029import java.util.HashMap; 030import java.util.LinkedHashMap; 031import java.util.List; 032import java.util.Locale; 033import java.util.Map; 034import java.util.Map.Entry; 035import java.util.Set; 036import java.util.stream.Collectors; 037 038import org.apache.avalon.framework.component.Component; 039import org.apache.avalon.framework.service.ServiceException; 040import org.apache.avalon.framework.service.ServiceManager; 041import org.apache.avalon.framework.service.Serviceable; 042import org.apache.cocoon.ProcessingException; 043import org.apache.commons.lang.StringUtils; 044import org.slf4j.LoggerFactory; 045 046import org.ametys.cms.repository.Content; 047import org.ametys.core.datasource.ConnectionHelper; 048import org.ametys.core.datasource.dbtype.SQLDatabaseTypeExtensionPoint; 049import org.ametys.core.ui.Callable; 050import org.ametys.core.user.UserIdentity; 051import org.ametys.core.util.I18nUtils; 052import org.ametys.core.util.URIUtils; 053import org.ametys.plugins.forms.Field; 054import org.ametys.plugins.forms.Field.FieldType; 055import org.ametys.plugins.forms.Form; 056import org.ametys.plugins.forms.FormsException; 057import org.ametys.plugins.forms.data.Answer; 058import org.ametys.plugins.forms.data.FieldValue; 059import org.ametys.plugins.forms.data.UserEntry; 060import org.ametys.plugins.forms.jcr.FormPropertiesManager; 061import org.ametys.plugins.repository.AmetysObjectResolver; 062import org.ametys.plugins.workflow.store.JdbcWorkflowStore; 063import org.ametys.runtime.config.Config; 064import org.ametys.runtime.i18n.I18nizableText; 065 066/** 067 * Class that handles creation and deletion of a table used by a form. 068 */ 069public class FormTableManager implements Component, Serviceable 070{ 071 /** Avalon Role */ 072 public static final String ROLE = FormTableManager.class.getName(); 073 074 /** The id of the configuration parameter used for the forms' datasource */ 075 public static final String FORMS_POOL_CONFIG_PARAM = "plugins.forms.datasource"; 076 077 /** Prefix for database tables */ 078 public static final String TABLE_PREFIX = "Forms_"; 079 080 /** ID field name. */ 081 public static final String ID_FIELD = "id"; 082 083 /** Creation date field name. */ 084 public static final String CREATION_DATE_FIELD = "creationDate"; 085 086 /** Current user login field name. */ 087 public static final String LOGIN_FIELD = "login"; 088 089 /** Current user population id field name. */ 090 public static final String POPULATION_ID_FIELD = "populationId"; 091 092 /** Workflow id column name. */ 093 public static final String WORKFLOW_ID_FIELD = "workflowId"; 094 095 /** Suffix for filename column. */ 096 public static final String FILE_NAME_COLUMN_SUFFIX = "-filename"; 097 098 /** Constant for table state */ 099 public static final int TABLE_CREATED_AND_UP_TO_DATE = 2; 100 101 /** Constant for table state */ 102 public static final int TABLE_CREATED_BUT_NEED_UPDATE = 3; 103 104 /** Constant for table state */ 105 public static final int TABLE_NOT_CREATED = 1; 106 107 /** Constant for table state */ 108 public static final int TABLE_UNKOWN_STATUS = 0; 109 110 private static org.slf4j.Logger __logger = LoggerFactory.getLogger(FormTableManager.class); 111 112 private ServiceManager _manager; 113 114 private FormPropertiesManager _formPropertiesManager; 115 private AmetysObjectResolver _resolver; 116 private I18nUtils _i18nUtils; 117 private JdbcWorkflowStore _jdbcWorkflowStore; 118 private SQLDatabaseTypeExtensionPoint _sqlDatabaseTypeExtensionPoint; 119 120 @Override 121 public void service(ServiceManager smanager) throws ServiceException 122 { 123 _manager = smanager; 124 } 125 126 private SQLDatabaseTypeExtensionPoint getSQLDatabaseTypeExtensionPoint() 127 { 128 if (_sqlDatabaseTypeExtensionPoint == null) 129 { 130 try 131 { 132 _sqlDatabaseTypeExtensionPoint = (SQLDatabaseTypeExtensionPoint) _manager.lookup(SQLDatabaseTypeExtensionPoint.ROLE); 133 } 134 catch (ServiceException e) 135 { 136 throw new RuntimeException(e); 137 } 138 } 139 return _sqlDatabaseTypeExtensionPoint; 140 } 141 142 private I18nUtils getI18nUtils() 143 { 144 if (_i18nUtils == null) 145 { 146 try 147 { 148 _i18nUtils = (I18nUtils) _manager.lookup(I18nUtils.ROLE); 149 } 150 catch (ServiceException e) 151 { 152 throw new RuntimeException(e); 153 } 154 } 155 return _i18nUtils; 156 } 157 158 private AmetysObjectResolver getAmetysObjectResolver() 159 { 160 if (_resolver == null) 161 { 162 try 163 { 164 _resolver = (AmetysObjectResolver) _manager.lookup(AmetysObjectResolver.ROLE); 165 } 166 catch (ServiceException e) 167 { 168 throw new RuntimeException(e); 169 } 170 } 171 return _resolver; 172 } 173 174 private FormPropertiesManager getFormPropertiesManager() 175 { 176 if (_formPropertiesManager == null) 177 { 178 try 179 { 180 _formPropertiesManager = (FormPropertiesManager) _manager.lookup(FormPropertiesManager.ROLE); 181 } 182 catch (ServiceException e) 183 { 184 throw new RuntimeException(e); 185 } 186 } 187 return _formPropertiesManager; 188 } 189 190 private JdbcWorkflowStore getJdbcWorkflowStore() 191 { 192 if (_jdbcWorkflowStore == null) 193 { 194 try 195 { 196 _jdbcWorkflowStore = (JdbcWorkflowStore) _manager.lookup(JdbcWorkflowStore.ROLE); 197 } 198 catch (ServiceException e) 199 { 200 throw new RuntimeException(e); 201 } 202 } 203 return _jdbcWorkflowStore; 204 } 205 206 /** 207 * Create database table to store results of content forms 208 * @param form informations used to create table 209 * @return true if the table was created false else 210 */ 211 public boolean createTable(Form form) 212 { 213 if (form == null) 214 { 215 throw new IllegalArgumentException("Form object can not be null"); 216 } 217 218 Map<String, FieldValue> columns = getColumns(form); 219 if (!_createOrUpdateTable(form.getId(), columns)) 220 { 221 return false; 222 } 223 return true; 224 } 225 226 /** 227 * Drop the table by its name 228 * @param table the name of the table to drop 229 * @return true if the table was dropped, false otherwise 230 */ 231 public boolean dropTable(String table) 232 { 233 PreparedStatement stmt = null; 234 Connection connection = null; 235 236 try 237 { 238 String dataSourceId = Config.getInstance().getValue(FORMS_POOL_CONFIG_PARAM); 239 connection = ConnectionHelper.getConnection(dataSourceId); 240 241 String request = "DROP TABLE " + getSQLDatabaseTypeExtensionPoint().languageEscapeTableName(ConnectionHelper.getDatabaseType(connection), table); 242 243 stmt = connection.prepareStatement(request); 244 stmt.executeUpdate(); 245 } 246 catch (SQLException e) 247 { 248 __logger.error("Error while deleting table " + table, e); 249 return false; 250 } 251 finally 252 { 253 ConnectionHelper.cleanup(stmt); 254 ConnectionHelper.cleanup(connection); 255 } 256 257 return true; 258 } 259 260 /** 261 * Get all the table names 262 * @return the list of all the forms' table names 263 */ 264 public List<String> getTableNames() 265 { 266 List<String> formsTableNames = new ArrayList<> (); 267 Connection con = null; 268 PreparedStatement stmt = null; 269 DatabaseMetaData metadata = null; 270 ResultSet tables = null; 271 272 try 273 { 274 // Use a connection pool to establish a connection to the data base 275 String dataSourceId = Config.getInstance().getValue(FORMS_POOL_CONFIG_PARAM); 276 con = ConnectionHelper.getConnection(dataSourceId); 277 metadata = con.getMetaData(); 278 tables = metadata.getTables(con.getCatalog(), con.getSchema(), "%", null); 279 280 while (tables.next()) 281 { 282 String tableName = tables.getString(3); 283 if (StringUtils.startsWithIgnoreCase(tableName, TABLE_PREFIX)) 284 { 285 formsTableNames.add(tableName); 286 } 287 } 288 } 289 catch (SQLException e) 290 { 291 __logger.error("Error while retrieving the forms table names", e); 292 } 293 finally 294 { 295 // Clean up connection resources 296 ConnectionHelper.cleanup(stmt); 297 ConnectionHelper.cleanup(con); 298 } 299 300 return formsTableNames; 301 } 302 303 /** 304 * Get the user answers from a list of forms 305 * @param forms the list of forms 306 * @param user the user 307 * @return the list of common answer 308 * @throws FormsException if an forms error occurred 309 */ 310 public List<Answer> getUserAnwsers(List<Form> forms, UserIdentity user) throws FormsException 311 { 312 List<Answer> anwsers = new ArrayList<>(); 313 314 // Get table names to avoid to make a request on non existant table 315 List<String> tableNames = getTableNames(); 316 317 Connection connection = null; 318 PreparedStatement stmt = null; 319 ResultSet rs = null; 320 321 try 322 { 323 String dataSourceId = Config.getInstance().getValue(FORMS_POOL_CONFIG_PARAM); 324 connection = ConnectionHelper.getConnection(dataSourceId); 325 String dbType = ConnectionHelper.getDatabaseType(connection); 326 327 List<String> queries = new ArrayList<>(); 328 for (Form form : forms) 329 { 330 String tableName = TABLE_PREFIX + form.getId(); 331 if (tableNames.stream().anyMatch(tableName::equalsIgnoreCase)) 332 { 333 StringBuilder sql = new StringBuilder(); 334 if (ConnectionHelper.DATABASE_DERBY.equals(dbType)) 335 { 336 sql.append("(SELECT CAST('"); 337 sql.append(form.getId()); 338 sql.append("' AS VARCHAR(128)) as formId, id, "); 339 } 340 else 341 { 342 sql.append("(SELECT '"); 343 sql.append(form.getId()); 344 sql.append("' as formId, id, "); 345 } 346 sql.append(LOGIN_FIELD); 347 sql.append(", "); 348 sql.append(POPULATION_ID_FIELD); 349 sql.append(", "); 350 sql.append(CREATION_DATE_FIELD); 351 sql.append(", "); 352 if (StringUtils.isNotBlank(form.getWorkflowName())) 353 { 354 sql.append(getSQLDatabaseTypeExtensionPoint().languageEscapeTableName(dbType, WORKFLOW_ID_FIELD)); 355 } 356 else 357 { 358 sql.append("-1 AS "); 359 sql.append(getSQLDatabaseTypeExtensionPoint().languageEscapeTableName(dbType, WORKFLOW_ID_FIELD)); 360 } 361 sql.append(" FROM "); 362 sql.append(getSQLDatabaseTypeExtensionPoint().languageEscapeTableName(dbType, tableName)); 363 sql.append(" WHERE "); 364 sql.append(LOGIN_FIELD); 365 sql.append(" = ? AND "); 366 sql.append(POPULATION_ID_FIELD); 367 sql.append(" = ?)"); 368 queries.add(sql.toString()); 369 } 370 else 371 { 372 __logger.warn("Table name '" + tableName + "' for content id '" + form.getContentId() + "' doesn't exist on the SQL database."); 373 } 374 } 375 376 stmt = connection.prepareStatement(queries.stream().collect(Collectors.joining(" UNION ALL "))); 377 378 int nextParam = 1; 379 for (Form form : forms) 380 { 381 String tableName = TABLE_PREFIX + form.getId(); 382 if (tableNames.stream().anyMatch(tableName::equalsIgnoreCase)) 383 { 384 stmt.setString(nextParam++, user.getLogin()); 385 stmt.setString(nextParam++, user.getPopulationId()); 386 } 387 } 388 389 // Execute the query. 390 rs = stmt.executeQuery(); 391 392 // Extract the result. 393 while (rs.next()) 394 { 395 String id = rs.getString("id"); 396 String formId = rs.getString("formId"); 397 Form form = getFormPropertiesManager().getForm(formId); 398 399 Timestamp creationDate = rs.getTimestamp(CREATION_DATE_FIELD); 400 Integer workflowId = rs.getInt(WORKFLOW_ID_FIELD); 401 402 Answer answer = new Answer( 403 id, 404 formId, 405 form.getLabel(), 406 creationDate, 407 form.getWorkflowName(), 408 workflowId 409 ); 410 411 anwsers.add(answer); 412 } 413 } 414 catch (Exception e) 415 { 416 __logger.error("Error while getting current user answers.", e); 417 throw new FormsException("Error while getting current user answers.", e); 418 } 419 finally 420 { 421 ConnectionHelper.cleanup(rs); 422 ConnectionHelper.cleanup(stmt); 423 ConnectionHelper.cleanup(connection); 424 } 425 426 return anwsers; 427 } 428 429 /** 430 * Get all the submissions for a form. 431 * @param form the form object. 432 * @param columns the columns 433 * @param offset The number of results to ignore. 434 * @param length The maximum number of results to return 435 * @param entryIds the ids of the submissions to retrieve 436 * @return the list of submissions. 437 * @throws FormsException if an error occurs. 438 */ 439 public List<UserEntry> getSubmissions(Form form, Map<String, FieldValue> columns, int offset, int length, List<Integer> entryIds) throws FormsException 440 { 441 List<UserEntry> entries = new ArrayList<>(); 442 443 final String tableName = TABLE_PREFIX + form.getId(); 444 445 Connection connection = null; 446 PreparedStatement stmt = null; 447 ResultSet rs = null; 448 449 try 450 { 451 452 String dataSourceId = Config.getInstance().getValue(FORMS_POOL_CONFIG_PARAM); 453 connection = ConnectionHelper.getConnection(dataSourceId); 454 String dbType = ConnectionHelper.getDatabaseType(connection); 455 456 String request = _getSubmissionsQuery(offset, length, tableName, dbType, form, columns); 457 458 stmt = connection.prepareStatement(request); 459 460 // Execute the query. 461 rs = stmt.executeQuery(); 462 463 // Extract the results. 464 while (rs.next()) 465 { 466 // First get the ID and submission date. 467 int id = rs.getInt(ID_FIELD); 468 if (entryIds == null || entryIds.contains(id)) 469 { 470 UserEntry entry = _getUserEntryFromResultSet(rs, form, columns, dbType); 471 entries.add(entry); 472 } 473 } 474 } 475 catch (SQLException | IOException e) 476 { 477 __logger.error("Error while getting entries for table " + tableName, e); 478 throw new FormsException("Error while getting entries for table " + tableName, e); 479 } 480 finally 481 { 482 ConnectionHelper.cleanup(rs); 483 ConnectionHelper.cleanup(stmt); 484 ConnectionHelper.cleanup(connection); 485 } 486 487 return entries; 488 } 489 490 /** 491 * Get the submissions query 492 * @param offset The number of results to ignore. 493 * @param length The maximum number of results to return 494 * @param tableName the name of the table 495 * @param dbType the type of the database used 496 * @param form the form 497 * @param columns the columns to query. Can be empty to query only common attributes 498 * @return the string query for getting the submissions of a form 499 */ 500 private String _getSubmissionsQuery(int offset, int length, final String tableName, String dbType, Form form, Map<String, FieldValue> columns) 501 { 502 StringBuilder query = _getFieldsForSelectQuery(dbType, form, columns); 503 504 query.append(" FROM "); 505 query.append(getSQLDatabaseTypeExtensionPoint().languageEscapeTableName(dbType, tableName)); 506 507 return getSQLDatabaseTypeExtensionPoint().languageLimitQuery(dbType, query.toString(), Integer.toString(length), Integer.toString(offset)); 508 } 509 510 /** 511 * Get the submission for a form. 512 * @param form the form object. 513 * @param columns the columns 514 * @param entryId the id of the submission to retrieve 515 * @return the submission. 516 * @throws FormsException if an error occurs. 517 */ 518 public UserEntry getSubmission(Form form, Map<String, FieldValue> columns, Integer entryId) throws FormsException 519 { 520 final String tableName = TABLE_PREFIX + form.getId(); 521 522 Connection connection = null; 523 PreparedStatement stmt = null; 524 ResultSet rs = null; 525 526 try 527 { 528 529 String dataSourceId = Config.getInstance().getValue(FORMS_POOL_CONFIG_PARAM); 530 connection = ConnectionHelper.getConnection(dataSourceId); 531 String dbType = ConnectionHelper.getDatabaseType(connection); 532 533 String request = _getSubmissionQuery(tableName, dbType, form, columns); 534 535 stmt = connection.prepareStatement(request); 536 stmt.setInt(1, entryId); 537 538 // Execute the query. 539 rs = stmt.executeQuery(); 540 541 // Extract the results. 542 while (rs.next()) 543 { 544 return _getUserEntryFromResultSet(rs, form, columns, dbType); 545 } 546 } 547 catch (SQLException | IOException e) 548 { 549 __logger.error("Error while getting entry with id '{}' for table '{}'", entryId, tableName, e); 550 throw new FormsException("Error while getting entry with id '" + entryId + "' for table " + tableName, e); 551 } 552 finally 553 { 554 ConnectionHelper.cleanup(rs); 555 ConnectionHelper.cleanup(stmt); 556 ConnectionHelper.cleanup(connection); 557 } 558 559 return null; 560 } 561 562 /** 563 * Get the submission query with entry id 564 * @param tableName the name of the table 565 * @param dbType the type of the database used 566 * @param form the form object. 567 * @param columns the columns to query. Can be empty to query only common attributes 568 * @return the string query for getting the submissions of a form 569 */ 570 private String _getSubmissionQuery(final String tableName, String dbType, Form form, Map<String, FieldValue> columns) 571 { 572 StringBuilder query = _getFieldsForSelectQuery(dbType, form, columns); 573 574 query.append(" FROM "); 575 query.append(getSQLDatabaseTypeExtensionPoint().languageEscapeTableName(dbType, tableName)); 576 query.append(" WHERE id = ?"); 577 578 return getSQLDatabaseTypeExtensionPoint().languageLimitQuery(dbType, query.toString(), "1", "0"); 579 } 580 581 private StringBuilder _getFieldsForSelectQuery(String dbType, Form form, Map<String, FieldValue> columns) 582 { 583 StringBuilder query = new StringBuilder("SELECT "); 584 query.append(ID_FIELD); 585 query.append(", "); 586 query.append(CREATION_DATE_FIELD); 587 query.append(", "); 588 query.append(LOGIN_FIELD); 589 query.append(", "); 590 query.append(POPULATION_ID_FIELD); 591 if (StringUtils.isNotBlank(form.getWorkflowName())) 592 { 593 query.append(", "); 594 query.append(getSQLDatabaseTypeExtensionPoint().languageEscapeTableName(dbType, WORKFLOW_ID_FIELD)); 595 } 596 597 for (String name : columns.keySet()) 598 { 599 FieldValue fieldValue = columns.get(name); 600 if (fieldValue.getType() == Types.BLOB) 601 { 602 // Only file name is needed. Blob will be required by an other way 603 String fileNameColumn = DbTypeHelper.normalizeName(dbType, name + FILE_NAME_COLUMN_SUFFIX); 604 query.append(", "); 605 query.append(getSQLDatabaseTypeExtensionPoint().languageEscapeTableName(dbType, fileNameColumn)); 606 } 607 else 608 { 609 query.append(", "); 610 query.append(getSQLDatabaseTypeExtensionPoint().languageEscapeTableName(dbType, name)); 611 } 612 } 613 614 return query; 615 } 616 617 /** 618 * Get a user entry from a result set 619 * @param rs the result set 620 * @param form the form 621 * @param columns the columns 622 * @param dbType the db type 623 * @return the user entry 624 * @throws SQLException if a sql error occurred 625 * @throws IOException is a io error occurred 626 */ 627 protected UserEntry _getUserEntryFromResultSet(ResultSet rs, Form form, Map<String, FieldValue> columns, String dbType) throws SQLException, IOException 628 { 629 // First get the ID and submission date. 630 int id = rs.getInt(ID_FIELD); 631 Timestamp creationDate = rs.getTimestamp(CREATION_DATE_FIELD); 632 633 UserIdentity userIdentity = null; 634 String login = rs.getString(LOGIN_FIELD); 635 String populationId = rs.getString(POPULATION_ID_FIELD); 636 if (StringUtils.isNotBlank(login) && StringUtils.isNotBlank(populationId)) 637 { 638 userIdentity = new UserIdentity(login, populationId); 639 } 640 641 List<FieldValue> entryValues = new ArrayList<>(); 642 643 for (Map.Entry<String, FieldValue> column : columns.entrySet()) 644 { 645 String columnLabel = column.getKey(); 646 FieldValue columnValue = column.getValue(); 647 648 // Extract the value. 649 FieldValue value = null; 650 651 switch (columnValue.getType()) 652 { 653 case Types.VARCHAR: 654 case Types.LONGVARCHAR: 655 String sValue = rs.getString(columnLabel); 656 value = new FieldValue(columnValue); 657 value.setValue(sValue); 658 entryValues.add(value); 659 break; 660 case Types.BOOLEAN: 661 Boolean bValue = rs.getBoolean(columnLabel); 662 value = new FieldValue(columnValue); 663 value.setValue(bValue); 664 entryValues.add(value); 665 break; 666 case Types.BLOB: 667 // Just get filename, blob will be requested by an other way if needed 668 String fileNameColumn = columnLabel + FILE_NAME_COLUMN_SUFFIX; 669 String normalizedName = DbTypeHelper.normalizeName(dbType, fileNameColumn); 670 String fileName = rs.getString(normalizedName); 671 value = new FieldValue(columnValue); 672 value.setValue(fileName); 673 674 entryValues.add(value); 675 break; 676 case Types.INTEGER: 677 Integer iValue = rs.getInt(columnLabel); 678 value = new FieldValue(columnValue); 679 value.setValue(iValue); 680 entryValues.add(value); 681 break; 682 default: 683 break; 684 } 685 } 686 687 Integer workflowId = null; 688 if (hasWorkflowIdColumn(form.getId())) 689 { 690 workflowId = rs.getInt(WORKFLOW_ID_FIELD); 691 } 692 693 return new UserEntry(id, creationDate, entryValues, workflowId, userIdentity); 694 } 695 696 /** 697 * Get the total count of submissions for a form. 698 * @param formId the id of the form 699 * @return the total count of submissions, or -1 if an error occurs. 700 * @throws FormsException if an error occurs. 701 */ 702 public int getTotalSubmissions(String formId) throws FormsException 703 { 704 final String tableName = TABLE_PREFIX + formId; 705 706 Connection connection = null; 707 PreparedStatement stmt = null; 708 ResultSet rs = null; 709 710 try 711 { 712 713 String dataSourceId = Config.getInstance().getValue(FORMS_POOL_CONFIG_PARAM); 714 connection = ConnectionHelper.getConnection(dataSourceId); 715 716 String dbType = ConnectionHelper.getDatabaseType(connection); 717 718 String request = "SELECT COUNT(*) FROM " + getSQLDatabaseTypeExtensionPoint().languageEscapeTableName(dbType, tableName); 719 720 stmt = connection.prepareStatement(request); 721 722 // Execute the query. 723 rs = stmt.executeQuery(); 724 725 // Extract the result. 726 if (rs.next()) 727 { 728 int count = rs.getInt(1); 729 return count; 730 } 731 } 732 catch (SQLException e) 733 { 734 __logger.error("Error while getting entries for table " + tableName, e); 735 throw new FormsException("Error while getting entries for table " + tableName, e); 736 } 737 finally 738 { 739 ConnectionHelper.cleanup(rs); 740 ConnectionHelper.cleanup(stmt); 741 ConnectionHelper.cleanup(connection); 742 } 743 744 return -1; 745 } 746 747 /** 748 * Get the forms of contents 749 * @param contentIds The ids of contents 750 * @param currentLanguage The current language 751 * @return The forms 752 * @throws FormsException if an error occurred while getting content' form 753 * @throws URISyntaxException if an error occurred when URL encoding form's label 754 */ 755 @Callable 756 public List<Map<String, Object>> getContentForms (List<String> contentIds, String currentLanguage) throws FormsException, URISyntaxException 757 { 758 List<Map<String, Object>> result = new ArrayList<>(); 759 760 for (String contentId : contentIds) 761 { 762 Content content = getAmetysObjectResolver().resolveById(contentId); 763 764 Map<String, Object> contentforms = new HashMap<>(); 765 766 contentforms.put("id", content.getId()); 767 contentforms.put("title", content.getTitle(new Locale(currentLanguage))); 768 769 List<Map<String, Object>> forms2json = new ArrayList<>(); 770 771 List<Form> forms = getFormPropertiesManager().getForms(content); 772 for (Form form : forms) 773 { 774 Map<String, Object> form2json = new HashMap<>(); 775 form2json.put("id", form.getId()); 776 form2json.put("label", URIUtils.decode(form.getLabel())); 777 form2json.put("workflowName", StringUtils.defaultString(form.getWorkflowName(), "")); 778 779 forms2json.add(form2json); 780 } 781 782 contentforms.put("forms", forms2json); 783 784 result.add(contentforms); 785 } 786 787 return result; 788 } 789 790 /** 791 * Get the columns information of a form 792 * @param siteName The site name 793 * @param formId the identifier of form 794 * @return The columns 795 * @throws FormsException if an error occurred 796 */ 797 @Callable 798 public List<Map<String, Object>> getColumns (String siteName, String formId) throws FormsException 799 { 800 List<Map<String, Object>> columns2json = new ArrayList<>(); 801 802 Form form = getFormPropertiesManager().getForm(siteName, formId); 803 804 Map<String, FieldValue> columns = getColumns(form); 805 for (FieldValue column : columns.values()) 806 { 807 Map<String, Object> column2json = new HashMap<>(); 808 column2json.put("id", column.getColumnName()); 809 810 Field field = column.getField(); 811 column2json.put("label", field.getLabel()); 812 column2json.put("type", field.getType().toString()); 813 column2json.put("name", field.getName()); 814 column2json.put("properties", field.getProperties()); 815 816 columns2json.add(column2json); 817 } 818 819 return columns2json; 820 } 821 822 /** 823 * Remove a list of tables 824 * @param tableNames the names of the tables to delete 825 * @throws FormsException if the drop of the tables is not successful 826 */ 827 @Callable 828 public void removeTables(List<String> tableNames) throws FormsException 829 { 830 for (String tableName : tableNames) 831 { 832 if (!dropTable(tableName)) 833 { 834 throw new FormsException("An error occurred occured while removing the tables from the database."); 835 } 836 } 837 } 838 839 /** 840 * Form to tableinfo. 841 * @param form the form. 842 * @return the tableinfo. 843 */ 844 public Map<String, FieldValue> getColumns(Form form) 845 { 846 Map<String, FieldValue> columns = new LinkedHashMap<>(); 847 848 for (Field field : form.getFields()) 849 { 850 final String id = field.getId(); 851 final String name = field.getName(); 852 853 FieldValue fdValue = null; 854 855 switch (field.getType()) 856 { 857 case TEXT: 858 case HIDDEN: 859 case PASSWORD: 860 case COST: 861 case SELECT: 862 fdValue = new FieldValue(id, Types.VARCHAR, null, field); 863 columns.put(id, fdValue); 864 break; 865 case TEXTAREA: 866 fdValue = new FieldValue(id, Types.LONGVARCHAR, null, field); 867 columns.put(id, fdValue); 868 break; 869 case RADIO: 870 if (!columns.containsKey(name)) 871 { 872 String value = field.getProperties().get("value"); 873 String label = field.getLabel(); 874 int index = 1; 875 field.getProperties().put("size", String.valueOf(index)); 876 field.getProperties().put("option-" + index + "-value", value); 877 field.getProperties().put("option-" + index + "-label", label); 878 879 field.getProperties().remove("value"); 880 881 fdValue = new FieldValue(name, Types.VARCHAR, null, field); 882 columns.put(name, fdValue); 883 } 884 else 885 { 886 // The value exists, clone it, concatenating the label. 887 if (StringUtils.isNotEmpty(field.getLabel())) 888 { 889 Field radioField = columns.get(name).getField(); 890 int index = Integer.parseInt(radioField.getProperties().get("size")); 891 index++; 892 893 String value = field.getProperties().get("value"); 894 String label = field.getLabel(); 895 896 radioField.getProperties().put("size", String.valueOf(index)); 897 radioField.getProperties().put("option-" + index + "-value", value); 898 radioField.getProperties().put("option-" + index + "-label", label); 899 900 Field dummyField = new Field(radioField.getId(), radioField.getType(), radioField.getName(), radioField.getLabel() + "/" + field.getLabel(), radioField.getProperties()); 901 columns.get(name).setField(dummyField); 902 } 903 } 904 break; 905 case CHECKBOX: 906 fdValue = new FieldValue(id, Types.BOOLEAN, null, field); 907 columns.put(id, fdValue); 908 break; 909 case FILE: 910 fdValue = new FieldValue(id, Types.BLOB, null, field); 911 columns.put(id, fdValue); 912 break; 913 case CAPTCHA: 914 break; 915 default: 916 break; 917 } 918 919 } 920 921 return columns; 922 } 923 924 /** 925 * Get the full column type corresponding to the database type (with precision). 926 * @param sqlType the sqltype value. 927 * @param dbType the database type. 928 * @return the real column type identifier (to be included in CREATE statement). 929 */ 930 protected String _getColumnType(int sqlType, String dbType) 931 { 932 switch (sqlType) 933 { 934 case Types.LONGVARCHAR: 935 return DbTypeHelper.getTextType(dbType); 936 case Types.BOOLEAN: 937 return DbTypeHelper.getBooleanType(dbType); 938 case Types.BLOB: 939 return DbTypeHelper.getBinaryType(dbType); 940 case Types.VARCHAR: 941 default: 942 return DbTypeHelper.getVarcharType(dbType); 943 } 944 } 945 946 /** 947 * Get the column type name (without precision) corresponding to the SQL type. 948 * @param sqlType the SQL type value. 949 * @param dbType the database type. 950 * @return the real column type name (without precision). 951 */ 952 protected String _getColumnTypeName(int sqlType, String dbType) 953 { 954 return StringUtils.substringBefore(_getColumnType(sqlType, dbType), "("); 955 } 956 957 /** 958 * Test if two columns have the same type. 959 * @param column the column retrieved from the database. 960 * @param newColumn the new column. 961 * @param dbType the database type. 962 * @return true if the two columns have the same type. 963 */ 964 protected boolean _isSameType(DbColumn column, FieldValue newColumn, String dbType) 965 { 966 int newColumnType = newColumn.getType(); 967 int currentColumnType = column.getSqlType(); 968 String newColumnTypeName = _getColumnTypeName(newColumnType, dbType); 969 String currentColumnTypeName = column.getTypeName(); 970 971 return currentColumnType == newColumnType || currentColumnTypeName.equals(newColumnTypeName); 972 } 973 974 private boolean _createTable(String formId, Map<String, FieldValue> columns) 975 { 976 String tableName = TABLE_PREFIX + formId; 977 978 Connection connection = null; 979 PreparedStatement stmt = null; 980 981 try 982 { 983 984 String dataSourceId = Config.getInstance().getValue(FORMS_POOL_CONFIG_PARAM); 985 connection = ConnectionHelper.getConnection(dataSourceId); 986 987 String dbType = ConnectionHelper.getDatabaseType(connection); 988 StringBuilder sql = new StringBuilder(); 989 990 sql.append("CREATE TABLE "); 991 sql.append(getSQLDatabaseTypeExtensionPoint().languageEscapeTableName(dbType, tableName)); 992 sql.append(" ( ").append(ID_FIELD).append(" ").append(DbTypeHelper.getIdentityType(dbType)).append(" ").append(DbTypeHelper.getIdentityMarker(dbType)).append(", "); 993 sql.append(CREATION_DATE_FIELD).append(" ").append(DbTypeHelper.getDateTimeType(dbType)).append(" NOT NULL,"); 994 sql.append(LOGIN_FIELD).append(" ").append(DbTypeHelper.getVarcharType(dbType)).append(" DEFAULT NULL,"); 995 sql.append(POPULATION_ID_FIELD).append(" ").append(DbTypeHelper.getVarcharType(dbType)).append(" DEFAULT NULL,"); 996 997 for (Map.Entry<String, FieldValue> column : columns.entrySet()) 998 { 999 int sqlType = column.getValue().getType(); 1000 sql.append(getSQLDatabaseTypeExtensionPoint().languageEscapeTableName(dbType, column.getKey())).append(" "); 1001 sql.append(_getColumnType(sqlType, dbType)); 1002 sql.append(" DEFAULT NULL,"); 1003 1004 // Add a column for the file name. 1005 if (sqlType == Types.BLOB) 1006 { 1007 String fileNameColumn = column.getKey() + FILE_NAME_COLUMN_SUFFIX; 1008 String normalizedName = DbTypeHelper.normalizeName(dbType, fileNameColumn); 1009 1010 sql.append(getSQLDatabaseTypeExtensionPoint().languageEscapeTableName(dbType, normalizedName)).append(" "); 1011 sql.append(DbTypeHelper.getVarcharType(dbType)); 1012 sql.append(" DEFAULT NULL,"); 1013 } 1014 } 1015 1016 sql.append("PRIMARY KEY (").append(ID_FIELD).append("))"); 1017 1018 if (__logger.isDebugEnabled()) 1019 { 1020 __logger.debug("Creating table : " + sql.toString()); 1021 } 1022 1023 stmt = connection.prepareStatement(sql.toString()); 1024 stmt.executeUpdate(); 1025 ConnectionHelper.cleanup(stmt); 1026 1027 // create sequence for oracle database 1028 if (ConnectionHelper.DATABASE_ORACLE.equals(dbType)) 1029 { 1030 String sqlQuery = "create sequence seq_" + formId; 1031 stmt = connection.prepareStatement(sqlQuery); 1032 stmt.executeUpdate(); 1033 } 1034 1035 return true; 1036 } 1037 catch (SQLException e) 1038 { 1039 __logger.error("Unable to create table " + tableName, e); 1040 return false; 1041 } 1042 finally 1043 { 1044 ConnectionHelper.cleanup(stmt); 1045 ConnectionHelper.cleanup(connection); 1046 } 1047 } 1048 1049 private boolean _alterTable(String formId, Map<String, FieldValue> newColumns) 1050 { 1051 String tableName = TABLE_PREFIX + formId; 1052 1053 Connection connection = null; 1054 String dataSourceId = Config.getInstance().getValue(FORMS_POOL_CONFIG_PARAM); 1055 1056 try 1057 { 1058 connection = ConnectionHelper.getConnection(dataSourceId); 1059 1060 // Start transaction. 1061 connection.setAutoCommit(false); 1062 1063 // Get the existing columns. 1064 Map<String, DbColumn> existingColumns = _getExistingColumns(connection, tableName); 1065 1066 // Compute the modifications to make. 1067 DbColumnModifications modifications = _getColumnsToModify(connection, existingColumns, newColumns); 1068 1069 // Move the columns that were deleted or replaced. 1070 _moveColumns(connection, modifications.getColumnsToRemove(), existingColumns, tableName); 1071 1072 // Add the new columns. 1073 _addColumns(connection, modifications.getColumnsToAdd(), tableName); 1074 1075 // Commit the transaction. 1076 connection.commit(); 1077 } 1078 catch (SQLException e) 1079 { 1080 __logger.error("Unable to alter table " + tableName, e); 1081 try 1082 { 1083 connection.rollback(); 1084 } 1085 catch (SQLException sqlex) 1086 { 1087 // Ignore. 1088 __logger.error("Error rollbacking the 'alter table' statements for table " + tableName, e); 1089 } 1090 return false; 1091 } 1092 finally 1093 { 1094 ConnectionHelper.cleanup(connection); 1095 } 1096 return true; 1097 1098 } 1099 1100 private int _checkTableStatus(String formId, Map<String, FieldValue> newColumns) 1101 { 1102 String tableName = TABLE_PREFIX + formId; 1103 1104 Connection connection = null; 1105 ResultSet tables = null; 1106 ResultSet rs = null; 1107 1108 Map<String, DbColumn> currentColumns = new HashMap<>(); 1109 1110 try 1111 { 1112 1113 String dataSourceId = Config.getInstance().getValue(FORMS_POOL_CONFIG_PARAM); 1114 connection = ConnectionHelper.getConnection(dataSourceId); 1115 1116 String dbType = ConnectionHelper.getDatabaseType(connection); 1117 1118 tableName = DbTypeHelper.filterName(dbType, tableName); 1119 1120 DatabaseMetaData metadata = connection.getMetaData(); 1121 tables = metadata.getTables(connection.getCatalog(), null, tableName, null); 1122 1123 if (!tables.next()) 1124 { 1125 return TABLE_NOT_CREATED; 1126 } 1127 1128 currentColumns = _getExistingColumns(connection, tableName); 1129 1130 for (String newColumn : newColumns.keySet()) 1131 { 1132 String filteredColumn = DbTypeHelper.filterName(dbType, newColumn); 1133 1134 // Test the existence of the column. 1135 if (!currentColumns.containsKey(filteredColumn)) 1136 { 1137 return TABLE_CREATED_BUT_NEED_UPDATE; 1138 } 1139 1140 // Test that the column type has not changed. 1141 DbColumn currentColumn = currentColumns.get(filteredColumn); 1142 FieldValue newColumnFv = newColumns.get(newColumn); 1143 1144 if (!_isSameType(currentColumn, newColumnFv, dbType)) 1145 { 1146 return TABLE_CREATED_BUT_NEED_UPDATE; 1147 } 1148 } 1149 1150 return TABLE_CREATED_AND_UP_TO_DATE; 1151 } 1152 catch (SQLException e) 1153 { 1154 __logger.error("Unable to get columns from table " + tableName, e); 1155 return TABLE_UNKOWN_STATUS; 1156 } 1157 finally 1158 { 1159 ConnectionHelper.cleanup(tables); 1160 ConnectionHelper.cleanup(rs); 1161 ConnectionHelper.cleanup(connection); 1162 } 1163 } 1164 1165 private boolean _createOrUpdateTable(String formId, final Map<String, FieldValue> columns) 1166 { 1167 boolean toReturn = true; 1168 switch (_checkTableStatus(formId, columns)) 1169 { 1170 case TABLE_NOT_CREATED: 1171 if (!_createTable(formId, columns)) 1172 { 1173 toReturn = false; 1174 } 1175 break; 1176 case TABLE_CREATED_BUT_NEED_UPDATE: 1177 if (!_alterTable(formId, columns)) 1178 { 1179 toReturn = false; 1180 } 1181 break; 1182 case TABLE_CREATED_AND_UP_TO_DATE: 1183 case TABLE_UNKOWN_STATUS: 1184 default: 1185 break; 1186 } 1187 return toReturn; 1188 } 1189 1190 private DbColumnModifications _getColumnsToModify(Connection con, Map<String, DbColumn> existingColumns, Map<String, FieldValue> newColumns) 1191 { 1192 DbColumnModifications modifications = new DbColumnModifications(); 1193 1194 String dbType = ConnectionHelper.getDatabaseType(con); 1195 1196 Map<String, FieldValue> columnsToAdd = new LinkedHashMap<>(); 1197 Map<String, DbColumn> columnsToRemove = new HashMap<>(); 1198 for (Map.Entry<String, FieldValue> newColumn : newColumns.entrySet()) 1199 { 1200 String filteredName = DbTypeHelper.filterName(dbType, newColumn.getKey()); 1201 columnsToAdd.put(filteredName, newColumn.getValue()); 1202 } 1203 1204 if (existingColumns != null) 1205 { 1206 for (String existingColName : existingColumns.keySet()) 1207 { 1208 DbColumn existingColumn = existingColumns.get(existingColName); 1209 FieldValue newColumn = columnsToAdd.get(existingColName); 1210 1211 // If the column does not already exist or if the type has changed, 1212 // mark the current column to be removed and keep the new column in the "to be added" list. 1213 if (newColumn != null && !_isSameType(existingColumn, newColumn, dbType)) 1214 { 1215 columnsToRemove.put(existingColName, existingColumn); 1216 } 1217 else 1218 { 1219 // The column already exists and its type has not changed: do not touch this one. 1220 columnsToAdd.remove(existingColName); 1221 } 1222 } 1223 } 1224 1225 modifications.setColumnsToAdd(columnsToAdd); 1226 modifications.setColumnsToRemove(columnsToRemove); 1227 1228 return modifications; 1229 } 1230 1231 private Map<String, DbColumn> _getExistingColumns(Connection con, String table) throws SQLException 1232 { 1233 ResultSet columns = null; 1234 Map<String, DbColumn> toReturn = new LinkedHashMap<>(); 1235 try 1236 { 1237 String dbType = ConnectionHelper.getDatabaseType(con); 1238 String filteredTableName = DbTypeHelper.filterName(dbType, table); 1239 1240 DatabaseMetaData metadata = con.getMetaData(); 1241 columns = metadata.getColumns(con.getCatalog(), null, filteredTableName, null); 1242 while (columns.next()) 1243 { 1244 String columnName = columns.getString("COLUMN_NAME"); 1245 Integer sqlType = columns.getInt("DATA_TYPE"); 1246 String typeName = columns.getString("TYPE_NAME"); 1247 Integer colSize = columns.getInt("COLUMN_SIZE"); 1248 1249 DbColumn col = new DbColumn(columnName, sqlType, typeName, colSize); 1250 1251 toReturn.put(columnName, col); 1252 } 1253 } 1254 catch (SQLException e) 1255 { 1256 __logger.error("Unable to get columns from " + table, e); 1257 throw e; 1258 } 1259 finally 1260 { 1261 ConnectionHelper.cleanup(columns); 1262 } 1263 return toReturn; 1264 } 1265 1266 private void _moveColumns(Connection con, Map<String, DbColumn> columnsToRemove, Map<String, DbColumn> existingColumns, String tableName) throws SQLException 1267 { 1268 String dbType = ConnectionHelper.getDatabaseType(con); 1269 PreparedStatement stmt = null; 1270 1271 Set<String> existingColumnNames = existingColumns.keySet(); 1272 1273 try 1274 { 1275 for (String columnName : columnsToRemove.keySet()) 1276 { 1277 DbColumn columnToRemove = columnsToRemove.get(columnName); 1278 1279 String newName = _getNewName(con, columnName, existingColumnNames); 1280 1281 String sql = DbTypeHelper.getRenameColumnStatement(tableName, columnToRemove, newName, dbType, getSQLDatabaseTypeExtensionPoint()); 1282 1283 if (__logger.isDebugEnabled()) 1284 { 1285 __logger.debug("Moving column: " + sql); 1286 } 1287 1288 stmt = con.prepareStatement(sql); 1289 stmt.executeUpdate(); 1290 1291 // Add a column for the file name. 1292 if (columnToRemove.getSqlType() == Types.BLOB) 1293 { 1294 // Release the previous statement. 1295 ConnectionHelper.cleanup(stmt); 1296 1297 String fileNameColumn = columnName + FILE_NAME_COLUMN_SUFFIX; 1298 String newFileNameColumn = _getNewName(con, fileNameColumn, existingColumnNames); 1299 String varcharType = DbTypeHelper.getVarcharType(dbType); 1300 1301 sql = DbTypeHelper.getRenameColumnStatement(tableName, fileNameColumn, newFileNameColumn, varcharType, dbType, getSQLDatabaseTypeExtensionPoint()); 1302 1303 if (__logger.isDebugEnabled()) 1304 { 1305 __logger.debug("Altering table : " + sql.toString()); 1306 } 1307 1308 stmt = con.prepareStatement(sql.toString()); 1309 stmt.executeUpdate(); 1310 } 1311 } 1312 } 1313 catch (SQLException e) 1314 { 1315 ConnectionHelper.cleanup(stmt); 1316 throw e; 1317 } 1318 } 1319 1320 private void _addColumns(Connection con, Map<String, FieldValue> columnsToAdd, String tableName) throws SQLException 1321 { 1322 String dbType = ConnectionHelper.getDatabaseType(con); 1323 PreparedStatement stmt = null; 1324 1325 try 1326 { 1327 for (Entry<String, FieldValue> column : columnsToAdd.entrySet()) 1328 { 1329 StringBuilder sql = new StringBuilder(); 1330 1331 sql.append("ALTER TABLE ").append(getSQLDatabaseTypeExtensionPoint().languageEscapeTableName(dbType, tableName)); 1332 1333 int sqlType = column.getValue().getType(); 1334 1335 sql.append(" ADD ").append(getSQLDatabaseTypeExtensionPoint().languageEscapeTableName(dbType, column.getKey())).append(" "); 1336 sql.append(_getColumnType(sqlType, dbType)); 1337 sql.append(" DEFAULT NULL"); 1338 1339 if (__logger.isDebugEnabled()) 1340 { 1341 __logger.debug("Altering table : " + sql.toString()); 1342 } 1343 1344 stmt = con.prepareStatement(sql.toString()); 1345 stmt.executeUpdate(); 1346 1347 // Add a column for the file name. 1348 if (sqlType == Types.BLOB) 1349 { 1350 // Release the previous statement. 1351 ConnectionHelper.cleanup(stmt); 1352 1353 sql.setLength(0); 1354 sql.append("ALTER TABLE ").append(getSQLDatabaseTypeExtensionPoint().languageEscapeTableName(dbType, tableName)); 1355 1356 String fileNameColumn = column.getKey() + FILE_NAME_COLUMN_SUFFIX; 1357 String normalizedName = DbTypeHelper.normalizeName(dbType, fileNameColumn); 1358 1359 sql.append(" ADD ").append(getSQLDatabaseTypeExtensionPoint().languageEscapeTableName(dbType, normalizedName)).append(" "); 1360 sql.append(DbTypeHelper.getVarcharType(dbType)); 1361 sql.append(" DEFAULT NULL"); 1362 1363 if (__logger.isDebugEnabled()) 1364 { 1365 __logger.debug("Adding column: " + sql.toString()); 1366 } 1367 1368 stmt = con.prepareStatement(sql.toString()); 1369 stmt.executeUpdate(); 1370 } 1371 } 1372 } 1373 catch (SQLException e) 1374 { 1375 ConnectionHelper.cleanup(stmt); 1376 throw e; 1377 } 1378 } 1379 1380 /** 1381 * Get the new name of a column to be moved. 1382 * @param con the connection. 1383 * @param currentName the current name. 1384 * @param existingColumnNames the names of the existing columns 1385 * @return the new name. 1386 */ 1387 private String _getNewName(Connection con, String currentName, Set<String> existingColumnNames) 1388 { 1389 String dbType = ConnectionHelper.getDatabaseType(con); 1390 1391 int i = 1; 1392 String newName = DbTypeHelper.normalizeName(dbType, currentName + "_old" + i); 1393 String filteredNewName = DbTypeHelper.filterName(dbType, newName); 1394 1395 while (existingColumnNames.contains(filteredNewName)) 1396 { 1397 i++; 1398 newName = DbTypeHelper.normalizeName(dbType, currentName + "_old" + i); 1399 filteredNewName = DbTypeHelper.filterName(dbType, newName); 1400 } 1401 1402 return newName; 1403 } 1404 1405 /** 1406 * Get the SQL type corresponding to a field type. 1407 * @param fieldType the field 1408 * @return the sql type as indicated in {@link java.sql.Types}. 1409 */ 1410 public static int getFieldSqlType(FieldType fieldType) 1411 { 1412 int sqlType = Types.VARCHAR; 1413 switch (fieldType) 1414 { 1415 case TEXT: 1416 case HIDDEN: 1417 case PASSWORD: 1418 case SELECT: 1419 case RADIO: 1420 sqlType = Types.VARCHAR; 1421 break; 1422 case TEXTAREA: 1423 sqlType = Types.LONGVARCHAR; 1424 break; 1425 case CHECKBOX: 1426 sqlType = Types.BOOLEAN; 1427 break; 1428 case FILE: 1429 sqlType = Types.BLOB; 1430 break; 1431 case CAPTCHA: 1432 sqlType = Types.OTHER; 1433 break; 1434 default: 1435 break; 1436 } 1437 return sqlType; 1438 } 1439 1440 /** 1441 * Add a workflow id column to the given table 1442 * @param formId the id of the form to alter 1443 * @throws FormsException if an error occurs 1444 */ 1445 public void addWorkflowIdColumn(String formId) throws FormsException 1446 { 1447 final String tableName = TABLE_PREFIX + formId; 1448 1449 Connection con = null; 1450 PreparedStatement stmt = null; 1451 1452 try 1453 { 1454 // Connect to the database. 1455 String dataSourceId = Config.getInstance().getValue(FORMS_POOL_CONFIG_PARAM); 1456 con = ConnectionHelper.getConnection(dataSourceId); 1457 String dbType = ConnectionHelper.getDatabaseType(con); 1458 StringBuilder sql = new StringBuilder(); 1459 1460 sql.append("ALTER TABLE ").append(getSQLDatabaseTypeExtensionPoint().languageEscapeTableName(dbType, tableName)); 1461 sql.append(" ADD ").append(getSQLDatabaseTypeExtensionPoint().languageEscapeTableName(dbType, WORKFLOW_ID_FIELD)).append(" "); 1462 sql.append(DbTypeHelper.getIntegerType(dbType)); 1463 1464 if (__logger.isDebugEnabled()) 1465 { 1466 __logger.debug("Adding column: " + sql.toString()); 1467 } 1468 1469 stmt = con.prepareStatement(sql.toString()); 1470 stmt.executeUpdate(); 1471 } 1472 catch (SQLException e) 1473 { 1474 __logger.error("Error while adding the workflow id column for the table " + tableName, e); 1475 throw new FormsException("Error while adding a column to the table " + tableName, e); 1476 } 1477 finally 1478 { 1479 // Clean up connection resources 1480 ConnectionHelper.cleanup(stmt); 1481 ConnectionHelper.cleanup(con); 1482 } 1483 } 1484 1485 /** 1486 * Delete the workflow id column of the given table 1487 * @param formId the id of the form to alter 1488 * @throws FormsException if an error occurs 1489 */ 1490 public void dropWorkflowIdColumn(String formId) throws FormsException 1491 { 1492 final String tableName = TABLE_PREFIX + formId; 1493 1494 Connection con = null; 1495 PreparedStatement stmt = null; 1496 1497 try 1498 { 1499 // Connect to the database. 1500 String dataSourceId = Config.getInstance().getValue(FORMS_POOL_CONFIG_PARAM); 1501 con = ConnectionHelper.getConnection(dataSourceId); 1502 String dbType = ConnectionHelper.getDatabaseType(con); 1503 StringBuilder sql = new StringBuilder(); 1504 1505 sql.append("ALTER TABLE ").append(getSQLDatabaseTypeExtensionPoint().languageEscapeTableName(dbType, tableName)); 1506 sql.append(" DROP COLUMN ").append(getSQLDatabaseTypeExtensionPoint().languageEscapeTableName(dbType, WORKFLOW_ID_FIELD)); 1507 1508 if (__logger.isDebugEnabled()) 1509 { 1510 __logger.debug("Deleting column: " + sql.toString()); 1511 } 1512 1513 stmt = con.prepareStatement(sql.toString()); 1514 stmt.executeUpdate(); 1515 } 1516 catch (SQLException e) 1517 { 1518 __logger.error("Error while deleting the workflow id column for the table " + tableName, e); 1519 throw new FormsException("Error while deleting a column from the table " + tableName, e); 1520 } 1521 finally 1522 { 1523 // Clean up connection resources 1524 ConnectionHelper.cleanup(stmt); 1525 ConnectionHelper.cleanup(con); 1526 } 1527 } 1528 1529 /** 1530 * Set a new workflow id to the column that has the given old workflow id 1531 * @param form the form 1532 * @param entryId the id of the entry 1533 * @param newWorkflowId the new workflow id 1534 * @throws FormsException if an error occurs 1535 */ 1536 public void setWorkflowId(Form form, long entryId, long newWorkflowId) throws FormsException 1537 { 1538 final String tableName = TABLE_PREFIX + form.getId(); 1539 1540 Connection con = null; 1541 PreparedStatement stmt = null; 1542 1543 try 1544 { 1545 // Connect to the database. 1546 String dataSourceId = Config.getInstance().getValue(FORMS_POOL_CONFIG_PARAM); 1547 con = ConnectionHelper.getConnection(dataSourceId); 1548 String dbType = ConnectionHelper.getDatabaseType(con); 1549 1550 String query = "UPDATE " + getSQLDatabaseTypeExtensionPoint().languageEscapeTableName(dbType, tableName); 1551 query += " SET " + getSQLDatabaseTypeExtensionPoint().languageEscapeTableName(dbType, WORKFLOW_ID_FIELD) + " = ?"; 1552 query += " WHERE " + ID_FIELD + " = ?"; 1553 1554 stmt = con.prepareStatement(query); 1555 1556 stmt.setLong(1, newWorkflowId); 1557 stmt.setLong(2, entryId); 1558 1559 // Execute the query. 1560 stmt.executeUpdate(); 1561 } 1562 catch (SQLException e) 1563 { 1564 __logger.error("Error while resetting the workflow id for the table " + tableName, e); 1565 throw new FormsException("Error while deleting entry for table " + tableName, e); 1566 } 1567 finally 1568 { 1569 // Clean up connection resources 1570 ConnectionHelper.cleanup(stmt); 1571 ConnectionHelper.cleanup(con); 1572 } 1573 } 1574 1575 /** 1576 * Get the workflow id of the given form entry or of all the entries 1577 * @param formId the id of the form 1578 * @param entryId the entry to get the workflow id from. If null, all the workflow ids are returned 1579 * @throws FormsException if an error occurs. 1580 * @return the list of workflow ids 1581 */ 1582 public List<Integer> getWorkflowIds(String formId, Integer entryId) throws FormsException 1583 { 1584 List<Integer> workflowIds = new ArrayList<> (); 1585 1586 final String tableName = TABLE_PREFIX + formId; 1587 1588 Connection con = null; 1589 PreparedStatement stmt = null; 1590 ResultSet rs = null; 1591 1592 try 1593 { 1594 if (!hasWorkflowIdColumn(formId)) 1595 { 1596 return Collections.EMPTY_LIST; 1597 } 1598 1599 // Connect to the database. 1600 String dataSourceId = Config.getInstance().getValue(FORMS_POOL_CONFIG_PARAM); 1601 con = ConnectionHelper.getConnection(dataSourceId); 1602 String dbType = ConnectionHelper.getDatabaseType(con); 1603 1604 String query = "SELECT " + getSQLDatabaseTypeExtensionPoint().languageEscapeTableName(dbType, "workflowId") + " FROM " + getSQLDatabaseTypeExtensionPoint().languageEscapeTableName(dbType, tableName); 1605 if (entryId != null) 1606 { 1607 query += " WHERE " + ID_FIELD + " = ?"; 1608 } 1609 1610 stmt = con.prepareStatement(query); 1611 1612 if (entryId != null) 1613 { 1614 stmt.setInt(1, entryId); 1615 } 1616 1617 // Execute the query. 1618 rs = stmt.executeQuery(); 1619 1620 // Extract the result(s). 1621 while (rs.next()) 1622 { 1623 workflowIds.add(rs.getInt(1)); 1624 } 1625 } 1626 catch (SQLException e) 1627 { 1628 __logger.error("Error while getting workflow ids from the table " + tableName, e); 1629 throw new FormsException("Error while getting workflow ids from the table " + tableName, e); 1630 } 1631 finally 1632 { 1633 // Clean up connection resources 1634 ConnectionHelper.cleanup(rs); 1635 ConnectionHelper.cleanup(stmt); 1636 ConnectionHelper.cleanup(con); 1637 } 1638 1639 return workflowIds; 1640 } 1641 1642 /** 1643 * Does the given form have a workflow id column in its SQL table ? 1644 * @param formId the id of the form 1645 * @return true if the form has a workflow id column, false otherwise 1646 * @throws SQLException if a database access error occurs 1647 */ 1648 public boolean hasWorkflowIdColumn (String formId) throws SQLException 1649 { 1650 boolean hasWorkflowId = true; 1651 1652 Connection con = null; 1653 final String tableName = TABLE_PREFIX + formId; 1654 1655 try 1656 { 1657 String dataSourceId = Config.getInstance().getValue(FORMS_POOL_CONFIG_PARAM); 1658 con = ConnectionHelper.getConnection(dataSourceId); 1659 Map<String, DbColumn> currentColumns = _getExistingColumns(con, tableName); 1660 1661 // Check if the column contains a workflow id 1662 if (!currentColumns.containsKey(WORKFLOW_ID_FIELD)) 1663 { 1664 hasWorkflowId = false; 1665 } 1666 1667 return hasWorkflowId; 1668 } 1669 finally 1670 { 1671 ConnectionHelper.cleanup(con); 1672 } 1673 } 1674 1675 /** 1676 * Delete a list of entries of a form 1677 * @param siteName The site name 1678 * @param formId The form id 1679 * @param entries The list of entries to delete 1680 * @throws ProcessingException if the given parameters are wrong 1681 * @throws FormsException if an error occurs when deleting the form submission 1682 */ 1683 @Callable 1684 public void deleteEntry(String siteName, String formId, List<Integer> entries) throws ProcessingException, FormsException 1685 { 1686 if (StringUtils.isEmpty(siteName) || StringUtils.isEmpty(formId)) 1687 { 1688 throw new ProcessingException("The site name and form ID must be provided."); 1689 } 1690 1691 Form form = getFormPropertiesManager().getForm(siteName, formId); 1692 1693 if (form == null) 1694 { 1695 throw new ProcessingException("The form of ID '" + formId + " can't be found in the site '" + siteName + "'."); 1696 } 1697 1698 final String tableName = TABLE_PREFIX + form.getId(); 1699 1700 Connection connection = null; 1701 PreparedStatement stmt = null; 1702 1703 try 1704 { 1705 String dataSourceId = Config.getInstance().getValue(FORMS_POOL_CONFIG_PARAM); 1706 connection = ConnectionHelper.getConnection(dataSourceId); 1707 String dbType = ConnectionHelper.getDatabaseType(connection); 1708 1709 StringBuilder sb = new StringBuilder(); 1710 sb.append("DELETE FROM " + getSQLDatabaseTypeExtensionPoint().languageEscapeTableName(dbType, tableName) + " WHERE "); 1711 1712 1713 int i; 1714 int count = entries.size(); 1715 for (i = 0; i < count; i++) 1716 { 1717 if (i > 0) 1718 { 1719 sb.append(" OR "); 1720 } 1721 sb.append(ID_FIELD).append("=?"); 1722 } 1723 1724 stmt = connection.prepareStatement(sb.toString()); 1725 1726 i = 1; 1727 List<Integer> workflowIds = new ArrayList<> (); 1728 for (Integer entryId : entries) 1729 { 1730 workflowIds.addAll(getWorkflowIds(formId, entryId)); 1731 stmt.setInt(i, entryId); 1732 i++; 1733 } 1734 1735 // Delete the corresponding workflow instances and their history 1736 for (Integer workflowId : workflowIds) 1737 { 1738 getJdbcWorkflowStore().clearHistory(workflowId); 1739 getJdbcWorkflowStore().deleteInstance(workflowId); 1740 } 1741 1742 // Execute the query. 1743 stmt.executeUpdate(); 1744 } 1745 catch (SQLException e) 1746 { 1747 __logger.error("Error while deleting entry for table " + tableName, e); 1748 throw new FormsException("Error while deleting entry for table " + tableName, e); 1749 } 1750 finally 1751 { 1752 ConnectionHelper.cleanup(stmt); 1753 ConnectionHelper.cleanup(connection); 1754 } 1755 } 1756 1757 /** 1758 * Clear all entries of the forms corresponding to the given list of ids. 1759 * @param siteName the name of the site. 1760 * @param formIds the list of form ids 1761 * @throws ProcessingException if an error occurs. 1762 * @throws FormsException if an error occurs. 1763 */ 1764 @Callable 1765 public void clearEntries(String siteName, List<String> formIds) throws ProcessingException, FormsException 1766 { 1767 if (StringUtils.isEmpty(siteName) || formIds.isEmpty()) 1768 { 1769 throw new ProcessingException("The site name and form ID must be provided."); 1770 } 1771 1772 for (String formId : formIds) 1773 { 1774 Form form = getFormPropertiesManager().getForm(siteName, formId); 1775 1776 if (form == null) 1777 { 1778 throw new ProcessingException("The form of ID '" + formId + " can't be found in the site '" + siteName + "'."); 1779 } 1780 1781 final String tableName = TABLE_PREFIX + form.getId(); 1782 1783 Connection connection = null; 1784 PreparedStatement stmt = null; 1785 1786 try 1787 { 1788 1789 String dataSourceId = Config.getInstance().getValue(FORMS_POOL_CONFIG_PARAM); 1790 connection = ConnectionHelper.getConnection(dataSourceId); 1791 String dbType = ConnectionHelper.getDatabaseType(connection); 1792 1793 String request = "DELETE FROM " + getSQLDatabaseTypeExtensionPoint().languageEscapeTableName(dbType, tableName); 1794 1795 stmt = connection.prepareStatement(request); 1796 1797 List<Integer> workflowIds = getWorkflowIds(formId, null); 1798 // Delete the corresponding workflow instances and their history 1799 for (Integer workflowId : workflowIds) 1800 { 1801 getJdbcWorkflowStore().clearHistory(workflowId); 1802 getJdbcWorkflowStore().deleteInstance(workflowId); 1803 } 1804 1805 // Execute the query. 1806 stmt.executeUpdate(); 1807 } 1808 catch (SQLException e) 1809 { 1810 __logger.error("Error while deleting entry for table " + tableName, e); 1811 throw new FormsException("Error while deleting entry for table " + tableName, e); 1812 } 1813 finally 1814 { 1815 ConnectionHelper.cleanup(stmt); 1816 ConnectionHelper.cleanup(connection); 1817 } 1818 } 1819 1820 } 1821 1822 /** 1823 * Get the SELECT statement for retrieving the form entries from the database. 1824 * @param siteName The site name 1825 * @param formId The form id 1826 * @return A result map with the SQL query and the table name 1827 * @throws ProcessingException if the given parameters are wrong 1828 */ 1829 @Callable 1830 public Map<String, String> getSelectStatement(String siteName, String formId) throws ProcessingException 1831 { 1832 Map<String, String> result = new HashMap<>(); 1833 1834 try 1835 { 1836 Form form = getFormPropertiesManager().getForm(siteName, formId); 1837 1838 if (form == null) 1839 { 1840 throw new ProcessingException("The form of ID '" + formId + " can't be found in the site '" + siteName + "'."); 1841 } 1842 1843 String tableName = FormTableManager.TABLE_PREFIX + form.getId(); 1844 1845 result.put("formId", form.getId()); 1846 result.put("tableName", tableName); 1847 1848 List<String> selectClause = new ArrayList<>(); 1849 1850 selectClause.add("id as Id"); 1851 selectClause.add("creationDate as '" + getI18nUtils().translate(new I18nizableText("plugin.forms", "PLUGINS_FORMS_CHOOSE_SHOW_FORM_SUBMISSION_DATE")) + "'"); 1852 1853 Map<String, FieldValue> columns = getColumns(form); 1854 for (FieldValue column : columns.values()) 1855 { 1856 selectClause.add(column.getColumnName() + " as '" + column.getField().getLabel() + "'"); 1857 } 1858 1859 StringBuilder request = new StringBuilder(); 1860 request.append("SELECT ").append(StringUtils.join(selectClause, ", ")).append("\nFROM ").append(tableName).append(";"); 1861 1862 result.put("query", request.toString()); 1863 } 1864 catch (FormsException e) 1865 { 1866 __logger.error("An error occurred while getting the results of a form.", e); 1867 throw new ProcessingException("An error occurred while getting the results of a form.", e); 1868 } 1869 1870 return result; 1871 } 1872 1873 /** 1874 * Class storing modifications to do on columns. 1875 */ 1876 class DbColumnModifications 1877 { 1878 1879 /** The columns to remove. */ 1880 protected Map<String, DbColumn> _columnsToRemove; 1881 1882 /** The columns to remove. */ 1883 protected Map<String, FieldValue> _columnsToAdd; 1884 1885 /** 1886 * Build a DbColumnModifications object. 1887 */ 1888 public DbColumnModifications() 1889 { 1890 this(new HashMap<String, DbColumn>(), new HashMap<String, FieldValue>()); 1891 } 1892 1893 /** 1894 * Build a DbColumnModifications object. 1895 * @param columnsToRemove the columns to remove. 1896 * @param columnsToAdd the columns to add. 1897 */ 1898 public DbColumnModifications(Map<String, DbColumn> columnsToRemove, Map<String, FieldValue> columnsToAdd) 1899 { 1900 this._columnsToRemove = columnsToRemove; 1901 this._columnsToAdd = columnsToAdd; 1902 } 1903 1904 /** 1905 * Get the columnsToRemove. 1906 * @return the columnsToRemove 1907 */ 1908 public Map<String, DbColumn> getColumnsToRemove() 1909 { 1910 return _columnsToRemove; 1911 } 1912 1913 /** 1914 * Set the columnsToRemove. 1915 * @param columnsToRemove the columnsToRemove to set 1916 */ 1917 public void setColumnsToRemove(Map<String, DbColumn> columnsToRemove) 1918 { 1919 this._columnsToRemove = columnsToRemove; 1920 } 1921 1922 /** 1923 * Get the columnsToAdd. 1924 * @return the columnsToAdd 1925 */ 1926 public Map<String, FieldValue> getColumnsToAdd() 1927 { 1928 return _columnsToAdd; 1929 } 1930 1931 /** 1932 * Set the columnsToAdd. 1933 * @param columnsToAdd the columnsToAdd to set 1934 */ 1935 public void setColumnsToAdd(Map<String, FieldValue> columnsToAdd) 1936 { 1937 this._columnsToAdd = columnsToAdd; 1938 } 1939 1940 } 1941 1942 /** 1943 * Class representing a db column. 1944 */ 1945 class DbColumn 1946 { 1947 1948 /** The columns to remove. */ 1949 protected String _name; 1950 1951 /** The columns to remove. */ 1952 protected int _sqlType; 1953 1954 /** The columns to remove. */ 1955 protected String _typeName; 1956 1957 /** The columns to remove. */ 1958 protected int _columnSize; 1959 1960 /** 1961 * Build a DB column object. 1962 */ 1963 public DbColumn() 1964 { 1965 this("", 0, "", 0); 1966 } 1967 1968 /** 1969 * Build a DB column object. 1970 * @param name the column name. 1971 * @param sqlType the sql type. 1972 * @param typeName the type name. 1973 * @param columnSize the column size. 1974 */ 1975 public DbColumn(String name, int sqlType, String typeName, int columnSize) 1976 { 1977 this._name = name; 1978 this._sqlType = sqlType; 1979 this._typeName = typeName; 1980 this._columnSize = columnSize; 1981 } 1982 1983 /** 1984 * Get the name. 1985 * @return the name 1986 */ 1987 public String getName() 1988 { 1989 return _name; 1990 } 1991 1992 /** 1993 * Set the name. 1994 * @param name the name to set 1995 */ 1996 public void setName(String name) 1997 { 1998 this._name = name; 1999 } 2000 2001 /** 2002 * Get the sqlType. 2003 * @return the sqlType 2004 */ 2005 public int getSqlType() 2006 { 2007 return _sqlType; 2008 } 2009 2010 /** 2011 * Set the sqlType. 2012 * @param sqlType the sqlType to set 2013 */ 2014 public void setSqlType(int sqlType) 2015 { 2016 this._sqlType = sqlType; 2017 } 2018 2019 /** 2020 * Get the typeName. 2021 * @return the typeName 2022 */ 2023 public String getTypeName() 2024 { 2025 return _typeName; 2026 } 2027 2028 /** 2029 * Set the typeName. 2030 * @param typeName the typeName to set 2031 */ 2032 public void setTypeName(String typeName) 2033 { 2034 this._typeName = typeName; 2035 } 2036 2037 /** 2038 * Get the columnSize. 2039 * @return the columnSize 2040 */ 2041 public int getColumnSize() 2042 { 2043 return _columnSize; 2044 } 2045 2046 /** 2047 * Set the columnSize. 2048 * @param columnSize the columnSize to set 2049 */ 2050 public void setColumnSize(int columnSize) 2051 { 2052 this._columnSize = columnSize; 2053 } 2054 2055 /** 2056 * Get a type identifier corresponding to the column (i.e. TEXT, VARCHAR(255), INT(1), and so on.) 2057 * @return a type identifier corresponding to the column (i.e. TEXT, VARCHAR(255), INT(1), and so on.) 2058 */ 2059 public String getColumnTypeIdentifier() 2060 { 2061 StringBuilder buff = new StringBuilder(); 2062 2063 buff.append(_typeName); 2064 if (_typeName.equals("VARCHAR") || _typeName.equals("INT") || _typeName.equals("NUMBER")) 2065 { 2066 buff.append('(').append(_columnSize).append(')'); 2067 } 2068 2069 return buff.toString(); 2070 } 2071 2072 } 2073 2074}