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