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