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