001/*
002 *  Copyright 2019 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.odfweb.cart;
017
018import java.sql.Connection;
019import java.sql.PreparedStatement;
020import java.sql.ResultSet;
021import java.sql.SQLException;
022import java.time.ZonedDateTime;
023import java.util.ArrayList;
024import java.util.Collection;
025import java.util.HashMap;
026import java.util.List;
027import java.util.Map;
028
029import org.apache.avalon.framework.component.Component;
030import org.apache.avalon.framework.configuration.Configurable;
031import org.apache.avalon.framework.configuration.Configuration;
032import org.apache.avalon.framework.configuration.ConfigurationException;
033import org.apache.commons.lang.StringUtils;
034
035import org.ametys.core.datasource.ConnectionHelper;
036import org.ametys.core.user.UserIdentity;
037import org.ametys.core.userpref.UserPreferencesException;
038import org.ametys.core.userpref.UserPreferencesStorage;
039import org.ametys.runtime.config.Config;
040import org.ametys.runtime.plugin.component.AbstractLogEnabled;
041
042
043/**
044 * Specific storage for odf cart user preferences
045 */
046public class ODFCartUserPreferencesStorage extends AbstractLogEnabled implements UserPreferencesStorage, Configurable, Component
047{
048    /** The Avalon Role */
049    public static final String ROLE = ODFCartUserPreferencesStorage.class.getName();
050    
051    /** The id of the data source used. */
052    protected String _dataSourceId;
053    
054    /** The login column, cannot be null. */
055    protected String _loginColumn;
056    
057    /** The population id column, cannot be null. */
058    protected String _populationColumn;
059    
060    /** The context column, can be null if the database is not context-dependent. */
061    protected String _contextColumn;
062    
063    /** The content id column */
064    protected String _contentIdColumn;
065    
066    /** Mapping from preference id to table name. */
067    protected Map<String, String> _prefIdToTable;
068
069    @Override
070    public void configure(Configuration configuration) throws ConfigurationException
071    {
072        // Data source id
073        Configuration dataSourceConf = configuration.getChild("datasource", false);
074        if (dataSourceConf == null)
075        {
076            throw new ConfigurationException("The 'datasource' configuration node must be defined.", dataSourceConf);
077        }
078        
079        String dataSourceConfParam = dataSourceConf.getValue();
080        String dataSourceConfType = dataSourceConf.getAttribute("type", "config");
081        
082        if (StringUtils.equals(dataSourceConfType, "config"))
083        {
084            _dataSourceId = Config.getInstance().getValue(dataSourceConfParam);
085        }
086        else // expecting type="id"
087        {
088            _dataSourceId = dataSourceConfParam;
089        }
090        
091        // Default to "contentId".
092        _contentIdColumn = configuration.getChild("content-id").getValue("contentId");
093        // Default to "login".
094        _loginColumn = configuration.getChild("loginColumn").getValue("login").toLowerCase();
095        // Default to "population"
096        _populationColumn = configuration.getChild("populationColumn").getValue("population").toLowerCase();
097        // Default to 'context' (no context column).
098        _contextColumn = configuration.getChild("contextColumn").getValue("context");
099        
100        // Configure the preference-table mappings.
101        configureMappings(configuration.getChild("mappings"));
102    }
103    
104    /**
105     * Configure the mappings from preference ID to table name.
106     * @param configuration the mapping configuration root.
107     * @throws ConfigurationException if an error occurs.
108     */
109    public void configureMappings(Configuration configuration) throws ConfigurationException
110    {
111        _prefIdToTable = new HashMap<>();
112        
113        for (Configuration mappingConf : configuration.getChildren("mapping"))
114        {
115            String prefId = mappingConf.getAttribute("prefId");
116            String table = mappingConf.getAttribute("table");
117            
118            _prefIdToTable.put(prefId, table);
119        }
120    }
121    
122    public Map<UserIdentity, Map<String, String>> getAllUnTypedUserPrefs(String storageContext, Map<String, String> contextVars) throws UserPreferencesException
123    {
124        Map<UserIdentity, Map<String, String>> allUserPrefs = new HashMap<>();
125        
126        for (String id : _prefIdToTable.keySet())
127        {
128            Map<UserIdentity, List<String>> preferences = _getUserPreferences(null, storageContext, id);
129            for (UserIdentity user : preferences.keySet())
130            {
131                Map<String, String> userPrefs = allUserPrefs.computeIfAbsent(user, __ -> new HashMap<>());
132                userPrefs.put(id, StringUtils.join(preferences.get(user), ","));
133            }
134        }
135        
136        return allUserPrefs;
137    }
138    
139    private Map<UserIdentity, List<String>> _getUserPreferences(UserIdentity user, String storageContext, String id) throws UserPreferencesException
140    {
141        Map<UserIdentity, List<String>> userResults = new HashMap<>();
142
143        Connection connection = null;
144        PreparedStatement statement = null;
145        ResultSet rs = null;
146        String table = _prefIdToTable.get(id);
147        
148        try
149        {
150            StringBuilder query = new StringBuilder();
151            query.append("SELECT ").append(" * FROM ").append(table).append(" WHERE ").append(_contextColumn).append(" = ?");
152            
153            if (user != null)
154            {
155                query.append(" AND ").append(_loginColumn).append(" = ? AND ").append(_populationColumn).append(" = ?");
156            }
157            
158            connection = ConnectionHelper.getConnection(_dataSourceId);
159            
160            statement = connection.prepareStatement(query.toString());
161            
162            int index = 1;
163            statement.setString(index, storageContext);
164            index++;
165            
166            if (user != null)
167            {
168                statement.setString(index, user.getLogin());
169                index++;
170                statement.setString(index, user.getPopulationId());
171            }
172            
173            rs = statement.executeQuery();
174            
175            while (rs.next())
176            {
177                UserIdentity u = _getUserIdentity(rs);
178                List<String> results = userResults.computeIfAbsent(u, __ -> new ArrayList<>());
179                results.add(rs.getString(_contentIdColumn));
180            }
181        }
182        catch (SQLException e)
183        {
184            String message = user == null
185                ? "Database error trying to get ODF cart all user' preferences in context '" + storageContext + "'."
186                : "Database error trying to get ODF cart preferences of user '" + user + "' in context '" + storageContext + "'.";
187            getLogger().error(message, e);
188            throw new UserPreferencesException(message, e);
189        }
190        finally
191        {
192            ConnectionHelper.cleanup(rs);
193            ConnectionHelper.cleanup(statement);
194            ConnectionHelper.cleanup(connection);
195        }
196        
197        return userResults;
198    }
199    
200    private UserIdentity _getUserIdentity(ResultSet rs) throws SQLException
201    {
202        String login = rs.getString(_loginColumn);
203        String population = rs.getString(_populationColumn);
204        return new UserIdentity(login, population);
205    }
206    
207    @Override
208    public Map<String, String> getUnTypedUserPrefs(UserIdentity user, String storageContext, Map<String, String> contextVars) throws UserPreferencesException
209    {
210        Map<String, String> prefs = new HashMap<>();
211        
212        for (String id : _prefIdToTable.keySet())
213        {
214            prefs.put(id, getUserPreferenceAsString(user, storageContext, contextVars, id));
215        }
216        
217        return prefs;
218    }
219    
220    /**
221     * Remove the stored user preferences for a login in a given context.
222     * @param prefIds the id of user preferences to remove
223     * @param user the user.
224     * @param storageContext the preferences storage context.
225     * @param contextVars the context variables.
226     * @throws UserPreferencesException if an error occurred
227     */
228    public void removeUserPreferences(Collection<String> prefIds, UserIdentity user, String storageContext, Map<String, String> contextVars) throws UserPreferencesException
229    {
230        Connection connection = null;
231        try
232        {
233            connection = ConnectionHelper.getConnection(_dataSourceId);
234            for (String id : prefIds)
235            {
236                _removeUserPreferencesFromTable(connection, user, storageContext, _prefIdToTable.get(id));
237            }
238        }
239        catch (SQLException e)
240        {
241            String message = "Database error trying to remove ODF cart preferences for user '" + user + "' in context '" + storageContext + "'.";
242            getLogger().error(message, e);
243            throw new UserPreferencesException(message, e);
244        }
245        finally
246        {
247            ConnectionHelper.cleanup(connection);
248        }
249    }
250    
251    @Override
252    public void removeUserPreferences(UserIdentity user, String storageContext, Map<String, String> contextVars) throws UserPreferencesException
253    {
254        Connection connection = null;
255        try
256        {
257            connection = ConnectionHelper.getConnection(_dataSourceId);
258            for (String id : _prefIdToTable.keySet())
259            {
260                _removeUserPreferencesFromTable(connection, user, storageContext, _prefIdToTable.get(id));
261            }
262        }
263        catch (SQLException e)
264        {
265            String message = "Database error trying to remove ODF cart preferences for user '" + user + "' in context '" + storageContext + "'.";
266            getLogger().error(message, e);
267            throw new UserPreferencesException(message, e);
268        }
269        finally
270        {
271            ConnectionHelper.cleanup(connection);
272        }
273    }
274    
275    private void _removeUserPreferencesFromTable(Connection connection, UserIdentity user, String storageContext, String table) throws SQLException
276    {
277        PreparedStatement stmt = null;
278        
279        try
280        {
281            StringBuilder query = new StringBuilder();
282            query.append("DELETE FROM ").append(table).append(" WHERE ").append(_loginColumn).append(" = ? AND ").append(_populationColumn).append(" = ?");
283            if (storageContext != null)
284            {
285                query.append(" AND ").append(_contextColumn).append(" = ?");
286            }
287            
288            stmt = connection.prepareStatement(query.toString());
289            stmt.setString(1, user.getLogin());
290            stmt.setString(2, user.getPopulationId());
291            if (storageContext != null)
292            {
293                stmt.setString(3, storageContext);
294            }
295            
296            stmt.executeUpdate();
297        }
298        finally
299        {
300            ConnectionHelper.cleanup(stmt);
301        }
302        
303    }
304
305    /**
306     * Remove content from all user preferences
307     * @param contentId the content id to remove
308     * @throws UserPreferencesException if failed to remove user preferences
309     */
310    public void removeContentFromUserPreferences(String contentId) throws UserPreferencesException
311    {
312        Connection connection = null;
313        PreparedStatement stmt = null;
314        
315        try
316        {
317            connection = ConnectionHelper.getConnection(_dataSourceId);
318            
319            StringBuilder query = new StringBuilder();
320            query.append("DELETE FROM ").append(_prefIdToTable.get(ODFCartManager.CART_USER_PREF_CONTENT_IDS)).append(" WHERE ").append(_contentIdColumn).append(" like ?");
321            
322            stmt = connection.prepareStatement(query.toString());
323            
324            stmt.setString(1, contentId + "%");
325            stmt.executeUpdate();
326            
327            ConnectionHelper.cleanup(stmt);
328            
329            query = new StringBuilder();
330            query.append("DELETE FROM ").append(_prefIdToTable.get(ODFCartManager.SUBSCRIPTION_USER_PREF_CONTENT_IDS)).append(" WHERE ").append(_contentIdColumn).append(" like ?");
331            
332            stmt = connection.prepareStatement(query.toString());
333            
334            stmt.setString(1, contentId + "%");
335            stmt.executeUpdate();
336            
337        }
338        catch (SQLException e)
339        {
340            String message = "Database error trying to remove ODF content of id '" + contentId + "' from all user's preferences";
341            getLogger().error(message, e);
342            throw new UserPreferencesException(message, e);
343        }
344        finally
345        {
346            ConnectionHelper.cleanup(stmt);
347            ConnectionHelper.cleanup(connection);
348        }
349    }
350
351    @Override
352    public void setUserPreferences(UserIdentity user, String storageContext, Map<String, String> contextVars, Map<String, String> preferences) throws UserPreferencesException
353    {
354        removeUserPreferences(preferences.keySet(), user, storageContext, contextVars);
355        
356        Connection connection = null;
357        try
358        {
359            connection = ConnectionHelper.getConnection(_dataSourceId);
360            _insertPreferences(connection, preferences, user, storageContext);
361        }
362        catch (SQLException e)
363        {
364            String message = "Database error trying to set ODF cart preferences of user '" + user + "' in context '" + storageContext + "'.";
365            getLogger().error(message, e);
366            throw new UserPreferencesException(message, e);
367        }
368        finally
369        {
370            ConnectionHelper.cleanup(connection);
371        }
372    }
373    
374    private void _insertPreferences(Connection connection, Map<String, String> preferences, UserIdentity user, String storageContext) throws SQLException
375    {
376        for (String id : preferences.keySet())
377        {
378            String table = _prefIdToTable.get(id);
379            
380            String[] contentIdsTab = StringUtils.split(preferences.get(id), ",");
381            
382            if (contentIdsTab.length > 0)
383            {
384                PreparedStatement stmt = null;
385                try
386                {
387                    StringBuilder query = new StringBuilder();
388                    query.append("INSERT INTO ").append(table).append("(").append(_loginColumn).append(", ").append(_populationColumn);
389                    query.append(", ").append(_contextColumn);
390                    query.append(", ").append(_contentIdColumn);
391    
392                    StringBuilder values = new StringBuilder();
393                    for (int i = 0; i < contentIdsTab.length; i++)
394                    {
395                        if (i != 0)
396                        {
397                            values.append(",");
398                        }
399                        values.append("(?, ?, ?, ?)");
400                    }
401                    
402                    query.append(") VALUES ").append(values);
403                    
404                    int i = 1;
405                    stmt = connection.prepareStatement(query.toString());
406                    
407                    for (String idContent : contentIdsTab)
408                    {
409                        stmt.setString(i++, user.getLogin());
410                        stmt.setString(i++, user.getPopulationId());
411                        stmt.setString(i++, storageContext);
412                        stmt.setString(i++, idContent);
413                    }
414                    
415                    stmt.executeUpdate();
416                }
417                finally
418                {
419                    ConnectionHelper.cleanup(stmt);
420                }
421            }
422        }
423    }
424
425    @Override
426    public String getUserPreferenceAsString(UserIdentity user, String storageContext, Map<String, String> contextVars, String id) throws UserPreferencesException
427    {
428        if (user == null)
429        {
430            return null;
431        }
432        
433        Map<UserIdentity, List<String>> userPreferences = _getUserPreferences(user, storageContext, id);
434        List<String> values = userPreferences.getOrDefault(user, null);
435        return values != null ? StringUtils.join(values, ",") : null;
436    }
437    
438    @Override
439    public Long getUserPreferenceAsLong(UserIdentity user, String storageContext, Map<String, String> contextVars, String id) throws UserPreferencesException
440    {
441        return null;
442    }
443
444    @Override
445    public ZonedDateTime getUserPreferenceAsDate(UserIdentity user, String storageContext, Map<String, String> contextVars, String id) throws UserPreferencesException
446    {
447        return null;
448    }
449
450    @Override
451    public Boolean getUserPreferenceAsBoolean(UserIdentity user, String storageContext, Map<String, String> contextVars, String id) throws UserPreferencesException
452    {
453        return null;
454    }
455
456    @Override
457    public Double getUserPreferenceAsDouble(UserIdentity user, String storageContext, Map<String, String> contextVars, String id) throws UserPreferencesException
458    {
459        return null;
460    }
461
462}