001/*
002 *  Copyright 2016 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.workflow.store;
017
018import java.sql.Connection;
019import java.sql.PreparedStatement;
020import java.sql.ResultSet;
021import java.sql.SQLException;
022import java.sql.Statement;
023import java.util.Collections;
024import java.util.HashMap;
025import java.util.List;
026import java.util.Map;
027
028import org.apache.avalon.framework.activity.Disposable;
029import org.apache.avalon.framework.activity.Initializable;
030import org.apache.avalon.framework.component.Component;
031import org.apache.avalon.framework.configuration.Configurable;
032import org.apache.avalon.framework.configuration.Configuration;
033import org.apache.avalon.framework.configuration.ConfigurationException;
034import org.apache.avalon.framework.service.ServiceException;
035import org.apache.avalon.framework.service.ServiceManager;
036import org.apache.avalon.framework.service.Serviceable;
037import org.apache.commons.lang.StringUtils;
038import org.slf4j.Logger;
039import org.slf4j.LoggerFactory;
040
041import org.ametys.core.datasource.ConnectionHelper;
042import org.ametys.core.datasource.SQLDataSourceManager;
043import org.ametys.plugins.workflow.JDBCPropertySet;
044import org.ametys.plugins.workflow.PropertySetExpression;
045import org.ametys.runtime.config.Config;
046
047import com.opensymphony.module.propertyset.PropertySet;
048import com.opensymphony.workflow.StoreException;
049import com.opensymphony.workflow.query.Expression;
050import com.opensymphony.workflow.query.NestedExpression;
051import com.opensymphony.workflow.query.WorkflowExpressionQuery;
052import com.opensymphony.workflow.spi.jdbc.JDBCWorkflowStore;
053
054/**
055 * Ametys JDBC workflow store
056 */
057public class JdbcWorkflowStore extends JDBCWorkflowStore implements AmetysWorkflowStore, Component, Serviceable, Configurable, Initializable, Disposable
058{
059    /** Avalon role. */
060    public static final String ROLE = JdbcWorkflowStore.class.getName();
061    
062    /** Logger */
063    protected Logger _logger = LoggerFactory.getLogger(JdbcWorkflowStore.class);
064    
065    /** The manager for SQL data source */
066    protected SQLDataSourceManager _sqlDataSourceManager;
067    
068    /** Configured datasource id */
069    protected String _dataSourceId;
070    
071    /** Configured data source type */
072    protected String _databaseType;
073    
074    /** from com.opensymphony.workflow.spi.jdbc.MySQLWorkflowStore */
075    protected String _entrySequenceIncrement;
076    /** from com.opensymphony.workflow.spi.jdbc.MySQLWorkflowStore */
077    protected String _entrySequenceRetrieve;
078    /** from com.opensymphony.workflow.spi.jdbc.MySQLWorkflowStore */
079    protected String _stepSequenceIncrement;
080    /** from com.opensymphony.workflow.spi.jdbc.MySQLWorkflowStore */
081    protected String _stepSequenceRetrieve;
082    
083    public void service(ServiceManager manager) throws ServiceException
084    {
085        _sqlDataSourceManager = (SQLDataSourceManager) manager.lookup(SQLDataSourceManager.ROLE);
086    }
087    
088    public void configure(Configuration configuration) throws ConfigurationException
089    {
090        Configuration dataSourceConf = configuration.getChild("datasource", true);
091        String dataSourceConfParam = dataSourceConf.getValue();
092        String dataSourceConfType = dataSourceConf.getAttribute("type", "config");
093        
094        if (StringUtils.equals(dataSourceConfType, "config"))
095        {
096            _dataSourceId = Config.getInstance().getValueAsString(dataSourceConfParam);
097        }
098        else // expecting type="id"
099        {
100            _dataSourceId = dataSourceConfParam;
101        }
102    }
103    
104    public void initialize() throws Exception
105    {
106        ds = _sqlDataSourceManager.getSQLDataSource(_dataSourceId);
107        
108        if (ds == null)
109        {
110            throw new ConfigurationException("Invalid datasource id: " + _dataSourceId);
111        }
112        
113        this.init(Collections.EMPTY_MAP);
114    }
115    
116    public void dispose()
117    {
118        _dataSourceId = null;
119        ds = null;
120    }
121    
122    @Override
123    public void init(Map props) throws StoreException
124    {
125        // Copier les bons paramètres pour JDBCWorkflowStore
126        entryTable = "OS_WFENTRY";
127        entryId = "ID";
128        entryName = "NAME";
129        entryState = "STATE";
130        historyTable = "OS_HISTORYSTEP";
131        historyPrevTable = "OS_HISTORYSTEP_PREV";
132        currentTable = "OS_CURRENTSTEP";
133        currentPrevTable = "OS_CURRENTSTEP_PREV";
134        stepId = "ID";
135        stepEntryId = "ENTRY_ID";
136        stepStepId = "STEP_ID";
137        stepActionId = "ACTION_ID";
138        stepOwner = "OWNER";
139        stepCaller = "CALLER";
140        stepStartDate = "START_DATE";
141        stepFinishDate = "FINISH_DATE";
142        stepDueDate = "DUE_DATE";
143        stepStatus = "STATUS";
144        stepPreviousId = "PREVIOUS_ID";
145        
146        String databaseType;
147        try
148        {
149            databaseType = getDatabaseType();
150        }
151        catch (SQLException e)
152        {
153            throw new StoreException("Unable to retrieve database type", e);
154        }
155        
156        // DATABASE_MYSQL
157        if (ConnectionHelper.DATABASE_MYSQL.equals(databaseType))
158        {
159            _entrySequenceIncrement = "INSERT INTO OS_ENTRYIDS (ID) values (null)";
160            _entrySequenceRetrieve = "SELECT LAST_INSERT_ID()";
161            _stepSequenceIncrement = "INSERT INTO OS_STEPIDS (ID) values (null)";
162            _stepSequenceRetrieve = "SELECT LAST_INSERT_ID()";
163        }
164        
165        // DATABASE_DERBY
166        else if (ConnectionHelper.DATABASE_DERBY.equals(databaseType))
167        {
168            _entrySequenceIncrement = "INSERT INTO OS_ENTRYIDS values (DEFAULT)";
169            _entrySequenceRetrieve = "VALUES IDENTITY_VAL_LOCAL()";
170            _stepSequenceIncrement = "INSERT INTO OS_STEPIDS values (DEFAULT)";
171            _stepSequenceRetrieve = "VALUES IDENTITY_VAL_LOCAL()";
172        }
173        
174        // DATABASE_POSTGRES
175        else if (ConnectionHelper.DATABASE_POSTGRES.equals(databaseType))
176        {
177            entrySequence = "SELECT nextval('seq_os_wfentry')";
178            stepSequence = "SELECT nextval('seq_os_currentsteps')";
179        }
180        
181        // DATABASE_ORACLE
182        else if (ConnectionHelper.DATABASE_ORACLE.equals(databaseType))
183        {
184            entrySequence = "SELECT seq_os_wfentry.nextval from dual";
185            stepSequence = "SELECT seq_os_currentsteps.nextval from dual";
186        }
187        
188        // DATABASE_HSQLDB
189        else if (ConnectionHelper.DATABASE_HSQLDB.equals(databaseType))
190        {
191            entrySequence = "call next value for seq_os_wfentry";
192            stepSequence = "call next value for seq_os_currentsteps";
193        }
194        
195        else
196        {
197            throw new IllegalArgumentException(String.format("Unsupported database type '%s'", databaseType));
198        }
199    }
200    
201    /**
202     * Database type getter
203     * @return The database type
204     * @throws SQLException on error
205     */
206    public String getDatabaseType() throws SQLException
207    {
208        if (_databaseType == null)
209        {
210            try (Connection connection = getConnection();)
211            {
212                _databaseType = ConnectionHelper.getDatabaseType(connection);
213            }
214        }
215        
216        return _databaseType;
217    }
218    
219    @Override
220    public PropertySet getPropertySet(long id)
221    {
222        // Utiliser notre propre property set (cocoon connection pool)
223        Map<String, String> args = new HashMap<>(1);
224        args.put("globalKey", JDBCPropertySet.OSWF_PREFIX + id);
225        
226        Map<String, Object> config = new HashMap<>(1);
227        config.put("datasource", ds);
228        
229        PropertySet ps = new JDBCPropertySet();
230        ps.init(config, args);
231        return ps;
232    }
233    
234    @Override
235    public boolean shouldClearHistory()
236    {
237        return true;
238    }
239    
240    @Override
241    protected void cleanup(Connection connection, Statement statement, ResultSet result)
242    {
243        ConnectionHelper.cleanup(result);
244        ConnectionHelper.cleanup(statement);
245        if (closeConnWhenDone)
246        {
247            ConnectionHelper.cleanup(connection);
248        }
249    }
250
251    @Override
252    public void deleteInstance(long idWorkflow)
253    {
254        Connection c = null;
255        PreparedStatement stmt = null;
256
257        try
258        {
259            // Récupérer la connexion du pool
260            c = getConnection();
261            // Supprimer les entrées récursivement grâce aux "ON DELETE CASCADE"
262            // Supprime dans l'ordre les lignes correspondantes des tables
263            // OS_HISTORYSTEP_PREV, OS_CURRENTSTEP_PREV, OS_HISTORYSTEP,
264            // OS_CURRENTSTEP
265            String sql = "DELETE FROM " + entryTable + " WHERE " + entryId + " = ?";
266            // Exécuter la requête
267            stmt = c.prepareStatement(sql);
268            // Positionner l'identifiant de l'instance du workflow
269            stmt.setLong(1, idWorkflow);
270
271            // Logger la requête
272            _logger.debug(sql);
273
274            // Vérifier si une ligne a bien été supprimée
275            if (stmt.executeUpdate() != 1)
276            {
277                _logger.error("Unable to remove a workflow instance in database");
278            }
279        }
280        catch (SQLException ex)
281        {
282            _logger.error("Error while removing a workflow instance", ex);
283        }
284        finally
285        {
286            cleanup(c, stmt, null);
287        }
288    }
289
290    public void clearHistory(long idWorkflow)
291    {
292        Connection c = null;
293        PreparedStatement stmt = null;
294
295        try
296        {
297            // Récupérer la connexion du pool
298            c = getConnection();
299            // Supprimer les entrées récursivement grâce aux "ON DELETE CASCADE"
300            // Supprime dans l'ordre les lignes correspondantes des tables
301            // OS_HISTORYSTEP_PREV, OS_CURRENTSTEP_PREV, OS_HISTORYSTEP
302            String sql = "DELETE FROM " + historyTable + " WHERE " + stepEntryId + " = ?";
303            // Exécuter la requête
304            stmt = c.prepareStatement(sql);
305            // Positionner l'identifiant de l'instance du workflow
306            stmt.setLong(1, idWorkflow);
307
308            // Logger la requête
309            _logger.debug(sql);
310
311            // Supprimer les éventuelles lignes
312            stmt.executeUpdate();
313        }
314        catch (SQLException e)
315        {
316            _logger.error("Error while clearing history steps from an workflow instance", e);
317        }
318        finally
319        {
320            cleanup(c, stmt, null);
321        }
322    }
323
324    @Override
325    public List query(WorkflowExpressionQuery e) throws StoreException
326    {
327        Expression expression = e.getExpression();
328
329        if (expression instanceof PropertySetExpression)
330        {
331            return JDBCPropertySet.query(ds, expression);
332        }
333        else if (expression instanceof NestedExpression)
334        {
335            NestedExpression expr = (NestedExpression) expression;
336
337            // Vérifier qu'il n'y ai pas autre chose que du PropertySet
338            // imbriqué avant de l'envoyer au JDBCPropertySet
339            if (JDBCPropertySet.isPropertySetExpressionsNested(expr))
340            {
341                return JDBCPropertySet.query(ds, expression);
342            }
343
344            return super.query(e);
345        }
346        else
347        {
348            return super.query(e);
349        }
350    }
351    
352    @Override
353    protected long getNextEntrySequence(Connection c) throws SQLException
354    {
355        // DATABASE_MYSQL - DATABASE_DERBY
356        String databaseType = getDatabaseType();
357        if (ConnectionHelper.DATABASE_MYSQL.equals(databaseType) || ConnectionHelper.DATABASE_DERBY.equals(databaseType))
358        {
359            PreparedStatement stmt = null;
360            ResultSet rset = null;
361            
362            try
363            {
364                stmt = c.prepareStatement(_entrySequenceIncrement);
365                stmt.executeUpdate();
366                rset = c.createStatement().executeQuery(_entrySequenceRetrieve);
367                
368                rset.next();
369                
370                long id = rset.getLong(1);
371                
372                return id;
373            }
374            finally
375            {
376                cleanup(null, stmt, rset);
377            }
378        }
379        else
380        {
381            return super.getNextEntrySequence(c);
382        }
383    }
384
385    @Override
386    protected long getNextStepSequence(Connection c) throws SQLException
387    {
388        // DATABASE_MYSQL - DATABASE_DERBY
389        String databaseType = getDatabaseType();
390        if (ConnectionHelper.DATABASE_MYSQL.equals(databaseType) || ConnectionHelper.DATABASE_DERBY.equals(databaseType))
391        {
392            PreparedStatement stmt = null;
393            ResultSet rset = null;
394            
395            try
396            {
397                stmt = c.prepareStatement(_stepSequenceIncrement);
398                stmt.executeUpdate();
399                rset = c.createStatement().executeQuery(_stepSequenceRetrieve);
400                
401                rset.next();
402                
403                long id = rset.getLong(1);
404                
405                return id;
406            }
407            finally
408            {
409                cleanup(null, stmt, rset);
410            }
411        }
412        else
413        {
414            return super.getNextEntrySequence(c);
415        }
416    }
417}