001/* 002 * Copyright 2015 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.datasource; 017 018import java.io.File; 019import java.io.FileInputStream; 020import java.io.FileNotFoundException; 021import java.io.IOException; 022import java.io.InputStream; 023import java.util.HashSet; 024import java.util.Set; 025 026import javax.sql.DataSource; 027 028import org.apache.avalon.framework.component.Component; 029import org.apache.avalon.framework.configuration.Configurable; 030import org.apache.avalon.framework.configuration.Configuration; 031import org.apache.avalon.framework.configuration.ConfigurationException; 032import org.apache.avalon.framework.context.ContextException; 033import org.apache.avalon.framework.context.Contextualizable; 034import org.apache.avalon.framework.service.ServiceException; 035import org.apache.avalon.framework.service.ServiceManager; 036import org.apache.avalon.framework.service.Serviceable; 037import org.apache.cocoon.Constants; 038import org.apache.cocoon.environment.Context; 039import org.apache.commons.lang3.StringUtils; 040import org.apache.commons.lang3.Strings; 041import org.apache.ibatis.builder.xml.XMLMapperBuilder; 042import org.apache.ibatis.mapping.Environment; 043import org.apache.ibatis.session.SqlSession; 044import org.apache.ibatis.session.SqlSessionFactory; 045import org.apache.ibatis.session.SqlSessionFactoryBuilder; 046import org.apache.ibatis.transaction.TransactionFactory; 047import org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory; 048 049import org.ametys.runtime.config.Config; 050import org.ametys.runtime.plugin.PluginsManager; 051import org.ametys.runtime.plugin.component.AbstractLogEnabled; 052import org.ametys.runtime.plugin.component.PluginAware; 053 054/** 055 * Interface to be implemented by any object that wishes to have 056 * access to one or multiple SqlMapClient. 057 */ 058public abstract class AbstractMyBatisDAO extends AbstractLogEnabled implements Contextualizable, Serviceable, PluginAware, Configurable, Component 059{ 060 /** The service manager */ 061 protected ServiceManager _manager; 062 063 private SqlSessionFactory _sessionFactory; 064 private SQLDataSourceManager _sqlDataSourceManager; 065 private String _contextPath; 066 private String _pluginName; 067 068 private String _dataSourceId; 069 070 private String _dataSourceParameter; 071 private boolean _dataSourceConfigurationParameter; 072 private Set<SqlMap> _sqlMaps; 073 074 public void contextualize(org.apache.avalon.framework.context.Context context) throws ContextException 075 { 076 Context ctx = (Context) context.get(Constants.CONTEXT_ENVIRONMENT_CONTEXT); 077 _contextPath = ctx.getRealPath("/"); 078 } 079 080 @Override 081 public void service(ServiceManager manager) throws ServiceException 082 { 083 _manager = manager; 084 } 085 086 private SQLDataSourceManager getSQLDataSourceManager() 087 { 088 if (_sqlDataSourceManager == null) 089 { 090 try 091 { 092 _sqlDataSourceManager = (SQLDataSourceManager) _manager.lookup(SQLDataSourceManager.ROLE); 093 } 094 catch (ServiceException e) 095 { 096 throw new RuntimeException(e); 097 } 098 } 099 return _sqlDataSourceManager; 100 } 101 102 public void setPluginInfo(String pluginName, String featureName, String id) 103 { 104 _pluginName = pluginName; 105 } 106 107 public void configure(Configuration configuration) throws ConfigurationException 108 { 109 _configureDatasource(configuration); 110 111 _sqlMaps = new HashSet<>(); 112 Configuration[] sqlMaps = configuration.getChildren("sqlMap"); 113 for (Configuration sqlMapConf : sqlMaps) 114 { 115 String resourceSrc = sqlMapConf.getAttribute("resource", null); 116 String configSrc = sqlMapConf.getAttribute("config", null); 117 118 if (StringUtils.isBlank(resourceSrc) && StringUtils.isBlank(configSrc)) 119 { 120 throw new ConfigurationException("The sqlmap configuration must have a 'resource' or 'config' attribute.", sqlMapConf); 121 } 122 123 if (StringUtils.isNotBlank(resourceSrc) && StringUtils.isNotBlank(configSrc)) 124 { 125 // If both 'resource' and 'config' attributes are set, try to find if the 'config' one exists, if so take it, if not, take the 'resource' one. 126 // This will enable to potentially override the kernel sqlMap ('resource') with the application sqlMap ('config') 127 File file = configSrc.startsWith("/") ? new File(_contextPath, configSrc) /* Absolute path */ 128 : new File(PluginsManager.getInstance().getPluginLocation(_pluginName), configSrc) /* Relative path */; 129 if (!file.isFile()) 130 { 131 configSrc = null; 132 } 133 } 134 135 SqlMap sqlMap = new SqlMap(); 136 137 if (StringUtils.isNotBlank(configSrc)) 138 { 139 sqlMap.setSource(configSrc); 140 sqlMap.setSourceType("config"); 141 } 142 else 143 { 144 sqlMap.setSource(resourceSrc); 145 sqlMap.setSourceType("resource"); 146 } 147 148 _sqlMaps.add(sqlMap); 149 } 150 } 151 152 /** 153 * Configure datasource 154 * @param configuration the configuration 155 * @throws ConfigurationException if an error occurred 156 */ 157 protected void _configureDatasource(Configuration configuration) throws ConfigurationException 158 { 159 Configuration dataSourceConf = configuration.getChild("datasource", false); 160 if (dataSourceConf == null) 161 { 162 throw new ConfigurationException("The 'datasource' configuration node must be defined.", dataSourceConf); 163 } 164 165 String dataSourceConfParam = dataSourceConf.getValue(); 166 String dataSourceConfType = dataSourceConf.getAttribute("type", "config"); 167 168 _dataSourceConfigurationParameter = Strings.CS.equals(dataSourceConfType, "config"); 169 _dataSourceParameter = dataSourceConfParam; 170 } 171 172 /** 173 * Reload configuration and object for mybatis 174 */ 175 protected synchronized void reload() 176 { 177 String newDatasourceId = _getDataSourceId(); 178 if (Strings.CS.equals(newDatasourceId, _dataSourceId)) 179 { 180 return; 181 } 182 183 // No it's not ok. Let's reload 184 _dataSourceId = newDatasourceId; 185 186 DataSource dataSource = getSQLDataSourceManager().getSQLDataSource(_dataSourceId); 187 if (dataSource == null) 188 { 189 throw new RuntimeException("Cannot (re)load MyBatis: Invalid datasource id: " + _dataSourceId); 190 } 191 192 SqlSessionFactoryBuilder sessionFactoryBuilder = new SqlSessionFactoryBuilder(); 193 194 TransactionFactory transactionFactory = new JdbcTransactionFactory(); 195 Environment env = new Environment(_dataSourceId, transactionFactory, dataSource); 196 197 org.apache.ibatis.session.Configuration config = _getMyBatisConfiguration(env); 198 199 for (SqlMap sqlMap : _sqlMaps) 200 { 201 String sourceType = sqlMap.getSourceType(); 202 String source = sqlMap.getSource(); 203 204 try 205 { 206 if ("config".equals(sourceType)) 207 { 208 File file = null; 209 if (source.startsWith("/")) 210 { 211 // Absolute path (from the root context path). 212 file = new File(_contextPath, source); 213 } 214 else 215 { 216 // Relative path 217 File pluginDir = PluginsManager.getInstance().getPluginLocation(_pluginName); 218 file = new File(pluginDir, source); 219 } 220 221 try (InputStream mapperStream = new FileInputStream(file)) 222 { 223 _initializeXMLMapper(mapperStream, file.toURI().toASCIIString(), config); 224 } 225 catch (FileNotFoundException e) 226 { 227 throw new RuntimeException("Cannot (re)load MyBatis: Cannot find configuration file: " + file, e); 228 } 229 catch (IOException e) 230 { 231 // Ignore 232 } 233 } 234 else 235 { 236 try (InputStream mapperStream = getClass().getResourceAsStream(source)) 237 { 238 _initializeXMLMapper(mapperStream, source, config); 239 } 240 catch (IOException e) 241 { 242 // Ignore 243 } 244 } 245 } 246 catch (Exception e) 247 { 248 // Consider it needs a reload next time the method is called 249 _dataSourceId = null; 250 throw e; 251 } 252 } 253 254 _sessionFactory = sessionFactoryBuilder.build(config); 255 } 256 257 private void _initializeXMLMapper(InputStream mapperStream, String mapperLocation, org.apache.ibatis.session.Configuration config) 258 { 259 if (getLogger().isInfoEnabled()) 260 { 261 getLogger().info("Initialized mybatis mapper at location '{}' for datasource id '{}'", mapperLocation, _dataSourceId); 262 } 263 264 XMLMapperBuilder mapperParser = new XMLMapperBuilder(mapperStream, config, mapperLocation, config.getSqlFragments()); 265 mapperParser.parse(); 266 } 267 268 /** 269 * Get the mybatis configuration 270 * @param env the mybatis environnement 271 * @return the mybatis configuration 272 */ 273 protected org.apache.ibatis.session.Configuration _getMyBatisConfiguration(Environment env) 274 { 275 org.apache.ibatis.session.Configuration config = new org.apache.ibatis.session.Configuration(env); 276 config.setCacheEnabled(true); 277 config.setLazyLoadingEnabled(true); 278 279 return config; 280 } 281 282 /** 283 * Get datasource id 284 * @return the datasource id 285 */ 286 protected String _getDataSourceId() 287 { 288 // Let's check if MyBatis current configuration is ok 289 String newDatasourceId; 290 if (_dataSourceConfigurationParameter) 291 { 292 newDatasourceId = Config.getInstance().getValue(_dataSourceParameter); 293 } 294 else 295 { 296 newDatasourceId = _dataSourceParameter; 297 } 298 299 if (getSQLDataSourceManager().getDefaultDataSourceId().equals(newDatasourceId)) 300 { 301 // resolve "default", as default may change 302 newDatasourceId = getSQLDataSourceManager().getDefaultDataSourceDefinition().getId(); 303 } 304 305 return newDatasourceId; 306 } 307 308 /** 309 * Returns the myBatis {@link SqlSession}. 310 * @return the myBatis {@link SqlSession}. 311 */ 312 protected SqlSession getSession() 313 { 314 return getSession(false); 315 } 316 317 /** 318 * Returns the myBatis {@link SqlSession}. 319 * @param autoCommit if the underlying Connection should auto commit statements. 320 * @return the myBatis {@link SqlSession}. 321 */ 322 protected SqlSession getSession(boolean autoCommit) 323 { 324 reload(); 325 return _sessionFactory.openSession(autoCommit); 326 } 327 328 class SqlMap 329 { 330 private String _source; 331 private String _sourceType; 332 333 public String getSource() 334 { 335 return _source; 336 } 337 338 public void setSource(String source) 339 { 340 _source = source; 341 } 342 343 public String getSourceType() 344 { 345 return _sourceType; 346 } 347 348 public void setSourceType(String sourceType) 349 { 350 _sourceType = sourceType; 351 } 352 } 353}