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