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.core.impl.userpref;
017
018import java.io.ByteArrayOutputStream;
019import java.io.IOException;
020import java.io.InputStream;
021import java.sql.Connection;
022import java.sql.PreparedStatement;
023import java.sql.ResultSet;
024import java.sql.SQLException;
025import java.util.Date;
026import java.util.HashMap;
027import java.util.Map;
028import java.util.Map.Entry;
029
030import javax.xml.transform.Result;
031import javax.xml.transform.TransformerException;
032import javax.xml.transform.TransformerFactory;
033import javax.xml.transform.sax.SAXTransformerFactory;
034import javax.xml.transform.sax.TransformerHandler;
035import javax.xml.transform.stream.StreamResult;
036
037import org.apache.avalon.framework.configuration.Configurable;
038import org.apache.avalon.framework.configuration.Configuration;
039import org.apache.avalon.framework.configuration.ConfigurationException;
040import org.apache.avalon.framework.logger.AbstractLogEnabled;
041import org.apache.avalon.framework.service.ServiceException;
042import org.apache.avalon.framework.service.ServiceManager;
043import org.apache.avalon.framework.service.Serviceable;
044import org.apache.avalon.framework.thread.ThreadSafe;
045import org.apache.cocoon.xml.AttributesImpl;
046import org.apache.cocoon.xml.XMLUtils;
047import org.apache.commons.lang.StringUtils;
048import org.apache.excalibur.xml.sax.SAXParser;
049import org.xml.sax.InputSource;
050import org.xml.sax.SAXException;
051
052import org.ametys.core.datasource.ConnectionHelper;
053import org.ametys.core.datasource.dbtype.SQLDatabaseTypeExtensionPoint;
054import org.ametys.core.user.UserIdentity;
055import org.ametys.core.userpref.DefaultUserPreferencesStorage;
056import org.ametys.core.userpref.UserPreferencesException;
057import org.ametys.core.userpref.UserPrefsHandler;
058import org.ametys.runtime.config.Config;
059import org.ametys.runtime.parameter.ParameterHelper;
060import org.ametys.runtime.parameter.ParameterHelper.ParameterType;
061
062/**
063 * The JDBC implementation of {@link DefaultUserPreferencesStorage}.
064 * This implementation stores preferences in a database, as an XML file.
065 */
066public class JdbcXmlUserPreferencesStorage extends AbstractLogEnabled implements DefaultUserPreferencesStorage, ThreadSafe, Configurable, Serviceable
067{
068    /** The id of the data source used. */
069    protected String _dataSourceId;
070    
071    /** The database table in which the preferences are stored. */
072    protected String _databaseTable;
073
074    /** Avalon service manager */
075    protected ServiceManager _manager;
076    
077    private SQLDatabaseTypeExtensionPoint _sqlDatabaseTypeExtensionPoint;
078    
079    @Override
080    public void configure(Configuration configuration) throws ConfigurationException
081    {
082     // Data source id
083        Configuration dataSourceConf = configuration.getChild("datasource", false);
084        if (dataSourceConf == null)
085        {
086            throw new ConfigurationException("The 'datasource' configuration node must be defined.", dataSourceConf);
087        }
088        
089        String dataSourceConfParam = dataSourceConf.getValue();
090        String dataSourceConfType = dataSourceConf.getAttribute("type", "config");
091        
092        if (StringUtils.equals(dataSourceConfType, "config"))
093        {
094            _dataSourceId = Config.getInstance().getValue(dataSourceConfParam);
095        }
096        else // expecting type="id"
097        {
098            _dataSourceId = dataSourceConfParam;
099        }
100        
101        _databaseTable = configuration.getChild("table").getValue();
102    }
103    
104    @Override
105    public void service(ServiceManager manager) throws ServiceException
106    {
107        _manager = manager;
108        _sqlDatabaseTypeExtensionPoint = (SQLDatabaseTypeExtensionPoint) manager.lookup(SQLDatabaseTypeExtensionPoint.ROLE);
109    }
110    
111    @Override
112    public Map<String, String> getUnTypedUserPrefs(UserIdentity user, String storageContext, Map<String, String> contextVars) throws UserPreferencesException
113    {
114        Map<String, String> prefs = new HashMap<>();
115        
116        Connection connection = null;
117        PreparedStatement stmt = null;
118        ResultSet rs = null;
119        SAXParser saxParser = null;
120        
121        try
122        {
123            connection = ConnectionHelper.getConnection(_dataSourceId);
124            String dbType = ConnectionHelper.getDatabaseType(connection);
125            
126            stmt = connection.prepareStatement("SELECT * FROM " + _databaseTable + " WHERE login = ? AND population = ? AND context = ?");
127            
128            stmt.setString(1, user.getLogin());
129            stmt.setString(2, user.getPopulationId());
130            stmt.setString(3, storageContext);
131            
132            rs = stmt.executeQuery();
133            
134            if (rs.next())
135            {
136                // Create the handler and fill the Map by parsing the configuration.
137                UserPrefsHandler handler = new UserPrefsHandler(prefs);
138                
139                saxParser = (SAXParser) _manager.lookup(SAXParser.ROLE);
140                if (ConnectionHelper.DATABASE_POSTGRES.equals(dbType))
141                {
142                    try (InputStream dataIs = rs.getBinaryStream("data"))
143                    {
144                        saxParser.parse(new InputSource(dataIs), handler);
145                    }
146                }
147                else
148                {
149                    try (InputStream dataIs = _sqlDatabaseTypeExtensionPoint.getBlob(dbType, rs, "data"))
150                    {
151                        saxParser.parse(new InputSource(dataIs), handler);
152                    }
153                }
154            }
155            
156            return prefs;
157        }
158        catch (ServiceException e)
159        {
160            String message = "Unable to get a SAX parser.";
161            getLogger().error(message, e);
162            throw new UserPreferencesException(message, e);
163        }
164        catch (SQLException e)
165        {
166            String message = "Database error trying to access the preferences of user '" + user + "' in context '" + storageContext + "'.";
167            getLogger().error(message, e);
168            throw new UserPreferencesException(message, e);
169        }
170        catch (SAXException | IOException e)
171        {
172            String message = "Error parsing the preferences of user '" + user + "' in context '" + storageContext + "'.";
173            getLogger().error(message, e);
174            throw new UserPreferencesException(message, e);
175        }
176        finally
177        {
178            ConnectionHelper.cleanup(rs);
179            ConnectionHelper.cleanup(stmt);
180            ConnectionHelper.cleanup(connection);
181            _manager.release(saxParser);
182        }
183    }
184    
185    @Override
186    public void removeUserPreferences(UserIdentity user, String storageContext, Map<String, String> contextVars) throws UserPreferencesException
187    {
188        Connection connection = null;
189        PreparedStatement stmt = null;
190        
191        try
192        {
193            connection = ConnectionHelper.getConnection(_dataSourceId);
194            
195            stmt = connection.prepareStatement("DELETE FROM " + _databaseTable + " WHERE login = ? AND population = ? AND context = ?");
196            stmt.setString(1, user.getLogin());
197            stmt.setString(2, user.getPopulationId());
198            stmt.setString(3, storageContext);
199            
200            stmt.executeUpdate();
201        }
202        catch (SQLException e)
203        {
204            String message = "Database error trying to remove preferences for login '" + user + "' in context '" + storageContext + "'.";
205            getLogger().error(message, e);
206            throw new UserPreferencesException(message, e);
207        }
208        finally
209        {
210            ConnectionHelper.cleanup(stmt);
211            ConnectionHelper.cleanup(connection);
212        }
213    }
214    
215    @Override
216    public void setUserPreferences(UserIdentity user, String storageContext, Map<String, String> contextVars, Map<String, String> preferences) throws UserPreferencesException
217    {
218        byte[] prefBytes = _getPreferencesXmlBytes(preferences);
219        Connection connection = null;
220        
221        try
222        {
223            
224            connection = ConnectionHelper.getConnection(_dataSourceId);
225            
226            String dbType = ConnectionHelper.getDatabaseType(connection);
227            
228            // Test if the preferences already exist.
229            boolean dataExists;
230            try (PreparedStatement stmt = connection.prepareStatement("SELECT count(*) FROM " + _databaseTable + " WHERE login = ? AND population = ? AND context = ?"))
231            {
232                stmt.setString(1, user.getLogin());
233                stmt.setString(2, user.getPopulationId());
234                stmt.setString(3, storageContext);
235                
236                try (ResultSet rs = stmt.executeQuery())
237                {
238                    rs.next();
239                    dataExists = rs.getInt(1) > 0;
240                }
241            }
242            
243            if (dataExists)
244            {
245                // If there's already a record, update it with the new data.
246                try (PreparedStatement stmt = connection.prepareStatement("UPDATE " + _databaseTable + " SET data = ? WHERE login = ? AND population = ? AND context = ?"))
247                {
248                    _sqlDatabaseTypeExtensionPoint.setBlob(dbType, stmt, 1, prefBytes);
249                    
250                    stmt.setString(2, user.getLogin());
251                    stmt.setString(3, user.getPopulationId());
252                    stmt.setString(4, storageContext);
253                    
254                    stmt.executeUpdate();
255                }
256            }
257            else
258            {
259                // If not, insert the data.
260                try (PreparedStatement stmt = connection.prepareStatement("INSERT INTO " + _databaseTable + "(login, population, context, data) VALUES(?, ?, ?, ?)"))
261                {
262                    stmt.setString(1, user.getLogin());
263                    stmt.setString(2, user.getPopulationId());
264                    stmt.setString(3, storageContext);
265                    _sqlDatabaseTypeExtensionPoint.setBlob(dbType, stmt, 4, prefBytes);
266                    
267                    stmt.executeUpdate();
268                }
269            }
270            
271        }
272        catch (SQLException e)
273        {
274            String message = "Database error trying to access the preferences of user '" + user + "' in context '" + storageContext + "'.";
275            getLogger().error(message, e);
276            throw new UserPreferencesException(message, e);
277        }
278        finally
279        {
280            ConnectionHelper.cleanup(connection);
281        }
282    }
283
284    @Override
285    public String getUserPreferenceAsString(UserIdentity user, String storageContext, Map<String, String> contextVars, String id) throws UserPreferencesException
286    {
287        String value = null;
288        
289        Map<String, String> values = getUnTypedUserPrefs(user, storageContext, contextVars);
290        if (values.containsKey(id))
291        {
292            value = values.get(id);
293        }
294        
295        return value;
296    }
297    
298    @Override
299    public Long getUserPreferenceAsLong(UserIdentity user, String storageContext, Map<String, String> contextVars, String id) throws UserPreferencesException
300    {
301        Long value = null;
302        
303        Map<String, String> values = getUnTypedUserPrefs(user, storageContext, contextVars);
304        if (values.containsKey(id))
305        {
306            value = (Long) ParameterHelper.castValue(values.get(id), ParameterType.LONG);
307        }
308        
309        return value;
310
311    }
312    
313    @Override
314    public Date getUserPreferenceAsDate(UserIdentity user, String storageContext, Map<String, String> contextVars, String id) throws UserPreferencesException
315    {
316        Date value = null;
317        
318        Map<String, String> values = getUnTypedUserPrefs(user, storageContext, contextVars);
319        if (values.containsKey(id))
320        {
321            value = (Date) ParameterHelper.castValue(values.get(id), ParameterType.DATE);
322        }
323        
324        return value;
325    }
326    
327    @Override
328    public Boolean getUserPreferenceAsBoolean(UserIdentity user, String storageContext, Map<String, String> contextVars, String id) throws UserPreferencesException
329    {
330        Boolean value = null;
331        
332        Map<String, String> values = getUnTypedUserPrefs(user, storageContext, contextVars);
333        if (values.containsKey(id))
334        {
335            value = (Boolean) ParameterHelper.castValue(values.get(id), ParameterType.BOOLEAN);
336        }
337        
338        return value;
339    }
340    
341    @Override
342    public Double getUserPreferenceAsDouble(UserIdentity user, String storageContext, Map<String, String> contextVars, String id) throws UserPreferencesException
343    {
344        Double value = null;
345        
346        Map<String, String> values = getUnTypedUserPrefs(user, storageContext, contextVars);
347        if (values.containsKey(id))
348        {
349            value = (Double) ParameterHelper.castValue(values.get(id), ParameterType.DOUBLE);
350        }
351        
352        return value;
353    }
354    
355    /**
356     * Write a Map of preferences as XML and return an InputStream on this XML.
357     * @param preferences the preferences Map.
358     * @return an InputStream on the preferences as XML.
359     * @throws UserPreferencesException if an error occurred
360     */
361    protected byte[] _getPreferencesXmlBytes(Map<String, String> preferences) throws UserPreferencesException
362    {
363        try
364        {
365            SAXTransformerFactory factory = (SAXTransformerFactory) TransformerFactory.newInstance();
366            TransformerHandler handler = factory.newTransformerHandler();
367            
368            ByteArrayOutputStream bos = new ByteArrayOutputStream();
369            Result result = new StreamResult(bos);
370            
371            handler.setResult(result);
372            
373            handler.startDocument();
374            
375            AttributesImpl attr = new AttributesImpl();
376            attr.addCDATAAttribute("version", "2");
377            XMLUtils.startElement(handler, "UserPreferences", attr);
378            
379            for (Entry<String, String> preference : preferences.entrySet())
380            {
381                String value = preference.getValue();
382                if (value != null)
383                {
384                    attr.clear();
385                    attr.addCDATAAttribute("id", preference.getKey());
386                    XMLUtils.createElement(handler, "preference", attr, preference.getValue());
387                }
388            }
389            
390            XMLUtils.endElement(handler, "UserPreferences");
391            handler.endDocument();
392            
393            return bos.toByteArray();
394        }
395        catch (TransformerException e)
396        {
397            throw new UserPreferencesException("Error writing the preferences as XML.", e);
398        }
399        catch (SAXException e)
400        {
401            throw new UserPreferencesException("Error writing the preferences as XML.", e);
402        }
403    }
404    
405}