001/* 002 * Copyright 2020 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.core.migration.storage.impl; 017 018import java.io.IOException; 019import java.io.InputStream; 020import java.sql.Connection; 021import java.sql.PreparedStatement; 022import java.sql.ResultSet; 023import java.sql.SQLException; 024import java.sql.Timestamp; 025import java.time.Instant; 026import java.util.ArrayList; 027import java.util.List; 028import java.util.Map; 029import java.util.Optional; 030import java.util.TreeMap; 031 032import org.apache.avalon.framework.configuration.Configuration; 033import org.apache.avalon.framework.configuration.ConfigurationException; 034import org.apache.avalon.framework.service.ServiceException; 035import org.apache.avalon.framework.service.ServiceManager; 036import org.apache.avalon.framework.service.Serviceable; 037import org.apache.commons.io.IOUtils; 038import org.apache.commons.lang3.StringUtils; 039import org.apache.excalibur.source.Source; 040import org.apache.excalibur.source.SourceResolver; 041 042import org.ametys.core.datasource.ConnectionHelper; 043import org.ametys.core.migration.MigrationException; 044import org.ametys.core.migration.NotMigrableInSafeModeException; 045import org.ametys.core.migration.configuration.VersionConfiguration; 046import org.ametys.core.migration.configuration.impl.SqlVersionConfiguration; 047import org.ametys.core.migration.storage.VersionStorage; 048import org.ametys.core.migration.version.Version; 049import org.ametys.core.migration.version.impl.SqlVersion; 050import org.ametys.core.script.SQLScriptHelper; 051import org.ametys.runtime.config.Config; 052import org.ametys.runtime.plugin.component.AbstractLogEnabled; 053 054/** 055 * SQL implementation of {@link VersionStorage}, to manage the list of versions in a database 056 */ 057public class SqlVersionStorage extends AbstractLogEnabled implements VersionStorage, Serviceable 058{ 059 private static final String __VERSION_TABLE_NAME = "Ametys_Versions"; 060 061 private static final String __COLUMN_COMPONENT = "component_identifier"; 062 private static final String __COLUMN_VERSION = "ametys_version_number"; 063 private static final String __COLUMN_DATE = "execution_date"; 064 private static final String __COLUMN_COMMENT = "execution_comment"; 065 066 private static final TreeMap<String, String> __AMETYS_VERSIONS_UDPATES = new TreeMap<>(); 067 static 068 { 069 __AMETYS_VERSIONS_UDPATES.put("20210401T0000", "Modify column 'component_identifier' to enlarge it to 250 characters."); 070 } 071 072 /** Source resolver */ 073 protected SourceResolver _sourceResolver; 074 075 @Override 076 public void service(ServiceManager manager) throws ServiceException 077 { 078 _sourceResolver = (SourceResolver) manager.lookup(SourceResolver.ROLE); 079 } 080 081 @Override 082 public Version getCurrentVersion(String componentIdentifier, VersionConfiguration versionConfiguration, String versionHandlerId) throws MigrationException, NotMigrableInSafeModeException 083 { 084 getLogger().debug("getCurrentVersion for component: {}", componentIdentifier); 085 086 return _getVersionsForComponent(componentIdentifier, versionConfiguration, versionHandlerId, true) 087 .stream() 088 .findFirst() 089 .orElse(null); 090 } 091 092 @Override 093 public List<Version> getAllVersions(String componentIdentifier, VersionConfiguration versionConfiguration, String versionHandlerId) throws MigrationException, NotMigrableInSafeModeException 094 { 095 getLogger().debug("getAllVersions for component: {}", componentIdentifier); 096 097 return _getVersionsForComponent(componentIdentifier, versionConfiguration, versionHandlerId, false); 098 } 099 100 /** 101 * Get the list of versions for a component 102 * @param componentIdentifier componentIdentifier 103 * @param versionConfiguration versionConfiguration 104 * @param versionHandlerId versionHandlerId 105 * @param onlyFirstVersion Only first version is returned 106 * @return a list of current Version, or an empty list if there is a configuration problem (no version is possible) 107 * @throws MigrationException Something went wrong 108 * @throws NotMigrableInSafeModeException Impossible to find the config 109 */ 110 private List<Version> _getVersionsForComponent(String componentIdentifier, Object versionConfiguration, String versionHandlerId, boolean onlyFirstVersion) throws MigrationException, NotMigrableInSafeModeException 111 { 112 if (!(versionConfiguration instanceof SqlVersionConfiguration)) 113 { 114 throw new MigrationException("The version configuration object should be an instance of SqlVersionConfiguration"); 115 } 116 String dataSourceId = ((SqlVersionConfiguration) versionConfiguration).getDataSourceId(); 117 Connection connection = null; 118 PreparedStatement stmt = null; 119 ResultSet rs = null; 120 121 try 122 { 123 _initVersionsTableIfNeeded(dataSourceId); 124 connection = ConnectionHelper.getConnection(dataSourceId); 125 return _getVersionsForComponent(connection, dataSourceId, componentIdentifier, versionHandlerId, onlyFirstVersion); 126 } 127 catch (SQLException e) 128 { 129 String message = String.format("Error while getting the versions for: %s (dataSource: %s)", componentIdentifier, dataSourceId); 130 throw new MigrationException(message, e); 131 } 132 finally 133 { 134 // Close connections 135 ConnectionHelper.cleanup(rs); 136 ConnectionHelper.cleanup(stmt); 137 ConnectionHelper.cleanup(connection); 138 } 139 } 140 141 private List<Version> _getVersionsForComponent(Connection connection, String dataSourceId, String componentIdentifier, String versionHandlerId, boolean onlyFirstVersion) throws SQLException 142 { 143 PreparedStatement stmt = null; 144 ResultSet rs = null; 145 146 try 147 { 148 String sql = "SELECT * FROM " + __VERSION_TABLE_NAME + " WHERE " + __COLUMN_COMPONENT + " = ? ORDER BY " + __COLUMN_VERSION + " DESC"; 149 150 stmt = connection.prepareStatement(sql); 151 stmt.setString(1, componentIdentifier); 152 153 rs = stmt.executeQuery(); 154 155 List<Version> versions = new ArrayList<>(); 156 157 while (rs.next() && (!onlyFirstVersion || versions.isEmpty())) 158 { 159 String versionNumber = rs.getString(__COLUMN_VERSION); 160 Timestamp timestamp = rs.getTimestamp(__COLUMN_DATE); 161 String comment = rs.getString(__COLUMN_COMMENT); 162 163 SqlVersion version = new SqlVersion(versionHandlerId, componentIdentifier, versionNumber, timestamp.toInstant(), comment, dataSourceId, ConnectionHelper.getDatabaseType(connection)); 164 versions.add(version); 165 } 166 167 // If only first version should be returned but the list is empty, create an empty version. 168 if (onlyFirstVersion && versions.isEmpty()) 169 { 170 // No version stored in SQL 171 versions.add(new SqlVersion(versionHandlerId, componentIdentifier, null, null, null, dataSourceId, ConnectionHelper.getDatabaseType(connection))); 172 } 173 174 return versions; 175 } 176 finally 177 { 178 ConnectionHelper.cleanup(rs); 179 ConnectionHelper.cleanup(stmt); 180 } 181 } 182 183 @Override 184 public void addVersion(Version version) throws MigrationException 185 { 186 getLogger().debug("Add version for: {}", version.toString()); 187 188 if (!(version instanceof SqlVersion)) 189 { 190 throw new MigrationException("Impossible to create a SQL version in a non-sql version: " + version.toString()); 191 } 192 193 SqlVersion sqlVersion = (SqlVersion) version; 194 String dataSourceId = sqlVersion.getDatasourceId(); 195 196 _initVersionsTableIfNeeded(dataSourceId); 197 198 Connection connection = null; 199 200 try 201 { 202 connection = ConnectionHelper.getConnection(dataSourceId); 203 _addVersion(connection, sqlVersion); 204 } 205 catch (SQLException e) 206 { 207 throw new MigrationException("Error during SQL query to set the new version for: " + version.toString() + " (dataSource: " + dataSourceId + ")", e); 208 } 209 finally 210 { 211 // Close connections 212 ConnectionHelper.cleanup(connection); 213 } 214 215 getLogger().debug("End add version for: {}", version.toString()); 216 } 217 218 private void _addVersion(Connection connection, Version version) throws SQLException 219 { 220 PreparedStatement stmt = null; 221 222 try 223 { 224 String sql = "INSERT INTO " + __VERSION_TABLE_NAME + " (" + __COLUMN_COMPONENT + ", " + __COLUMN_VERSION + ", " + __COLUMN_DATE + ", " + __COLUMN_COMMENT + ") VALUES (?, ?, ?, ?)"; 225 226 stmt = connection.prepareStatement(sql); 227 stmt.setString(1, version.getComponentId()); 228 stmt.setString(2, version.getVersionNumber()); 229 stmt.setTimestamp(3, Timestamp.from(version.getExecutionInstant())); 230 stmt.setString(4, version.getComment()); 231 232 stmt.executeUpdate(); 233 } 234 finally 235 { 236 // Close connections 237 ConnectionHelper.cleanup(stmt); 238 } 239 } 240 241 @Override 242 public void removeAllVersions(String componentIdentifier, VersionConfiguration versionConfiguration) throws MigrationException 243 { 244 getLogger().debug("Start remove all version for component: {}", componentIdentifier); 245 246 if (!(versionConfiguration instanceof SqlVersionConfiguration)) 247 { 248 throw new MigrationException("The version configuration object should be an instance of SqlVersionConfiguration"); 249 } 250 String dataSourceId = ((SqlVersionConfiguration) versionConfiguration).getDataSourceId(); 251 Connection connection = null; 252 PreparedStatement stmt = null; 253 254 try 255 { 256 _initVersionsTableIfNeeded(dataSourceId); 257 258 connection = ConnectionHelper.getConnection(dataSourceId); 259 String sql = "DELETE FROM " + __VERSION_TABLE_NAME + " WHERE " + __COLUMN_COMPONENT + " = ?"; 260 261 stmt = connection.prepareStatement(sql); 262 stmt.setString(1, componentIdentifier); 263 264 stmt.executeUpdate(); 265 } 266 catch (SQLException e) 267 { 268 String message = String.format("Error while getting the versions for %s (datasource: %s)", componentIdentifier, dataSourceId); 269 throw new MigrationException(message, e); 270 } 271 finally 272 { 273 // Close connections 274 ConnectionHelper.cleanup(stmt); 275 ConnectionHelper.cleanup(connection); 276 } 277 278 getLogger().debug("End remove all version for component: {}", componentIdentifier); 279 } 280 281 /** 282 * Create the versions table if needed 283 * @param dataSourceId the datasource where the table will be created 284 * @throws MigrationException Something went wrong 285 */ 286 protected void _initVersionsTableIfNeeded(String dataSourceId) throws MigrationException 287 { 288 getLogger().debug("Start initVersionsTableIfNeeded"); 289 290 Connection connection = null; 291 try 292 { 293 String componentId = "ametys.versions.table"; 294 String maxVersionNumber = __AMETYS_VERSIONS_UDPATES.lastKey(); 295 296 connection = ConnectionHelper.getConnection(dataSourceId); 297 298 String databaseType = ConnectionHelper.getDatabaseType(connection); 299 300 // Test and create tables 301 if (SQLScriptHelper.createTableIfNotExists(connection, __VERSION_TABLE_NAME, "plugin:core://scripts/%s/ametys_versions.sql", _sourceResolver)) 302 { 303 // Add the higher version 304 SqlVersion version = new SqlVersion("sql", componentId, maxVersionNumber, Instant.now(), "Automatic Initialization.", dataSourceId, databaseType); 305 _addVersion(connection, version); 306 } 307 else 308 { 309 // Get the current version 310 String currentVersion = _getVersionsForComponent(connection, dataSourceId, componentId, "sql", true) 311 .stream() 312 .findFirst() 313 .map(Version::getVersionNumber) 314 // If no version, all upgrades should be applied 315 .orElse("0"); 316 317 // Apply missing upgrades 318 String nextVersion = __AMETYS_VERSIONS_UDPATES.higherKey(currentVersion); 319 if (nextVersion != null) 320 { 321 for (Map.Entry<String, String> versionEntry : __AMETYS_VERSIONS_UDPATES.tailMap(nextVersion).entrySet()) 322 { 323 String versionNumber = versionEntry.getKey(); 324 String comment = versionEntry.getValue(); 325 getLogger().info("Upgrade table '{}' to version '{}' for component '{}' on datasource '{}'{}", __VERSION_TABLE_NAME, versionNumber, componentId, dataSourceId, StringUtils.isNotBlank(comment) ? " (" + comment + ")" : StringUtils.EMPTY); 326 327 Source source = null; 328 try 329 { 330 source = _sourceResolver.resolveURI(String.format("plugin:core://scripts/%s/ametys_versions/v%s.sql", databaseType, versionNumber)); 331 try (InputStream is = source.getInputStream()) 332 { 333 String script = IOUtils.toString(is, "UTF-8"); 334 SQLScriptHelper.runScript(connection, script); 335 336 comment = Optional.ofNullable(comment) 337 .filter(StringUtils::isNotBlank) 338 .map(c -> "Automatic Upgrade: " + c) 339 .orElse("Automatic Upgrade."); 340 341 SqlVersion version = new SqlVersion("sql", componentId, versionNumber, Instant.now(), comment, dataSourceId, databaseType); 342 _addVersion(connection, version); 343 } 344 } 345 finally 346 { 347 _sourceResolver.release(source); 348 } 349 } 350 } 351 else 352 { 353 getLogger().debug("The table {} of datasource '{}' is already up to date", __VERSION_TABLE_NAME, dataSourceId); 354 } 355 } 356 } 357 catch (SQLException | IOException e) 358 { 359 String errorMsg = String.format("Error during SQL tables initialization for data source id: '%s'.", StringUtils.defaultString(dataSourceId)); 360 throw new MigrationException(errorMsg, e); 361 } 362 finally 363 { 364 ConnectionHelper.cleanup(connection); 365 } 366 367 getLogger().debug("End initVersionsTableIfNeeded"); 368 } 369 370 public VersionConfiguration getConfiguration(String componentId, Configuration versionConfiguration) throws ConfigurationException, NotMigrableInSafeModeException 371 { 372 String dataSourceId = null; 373 Configuration dataSourceConf = versionConfiguration.getChild("datasource"); 374 String dataSourceConfParam = dataSourceConf.getValue(null); 375 if (StringUtils.isBlank(dataSourceConfParam)) 376 { 377 throw new ConfigurationException("The 'datasource' configuration node must be defined and contain non empty value.", dataSourceConf); 378 } 379 380 String dataSourceConfType = dataSourceConf.getAttribute("type", null); 381 382 if (StringUtils.equals(dataSourceConfType, "config")) 383 { 384 if (Config.getInstance() != null) 385 { 386 dataSourceId = Config.getInstance().getValue(dataSourceConfParam); 387 } 388 else 389 { 390 throw new NotMigrableInSafeModeException("Config is needed to migrate " + componentId); 391 } 392 } 393 else // expecting type="id" 394 { 395 dataSourceId = dataSourceConfParam; 396 } 397 398 return new SqlVersionConfiguration(dataSourceId); 399 } 400}