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}