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