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