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}