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.InputStream;
022import java.util.HashSet;
023import java.util.Set;
024
025import javax.sql.DataSource;
026
027import org.apache.avalon.framework.component.Component;
028import org.apache.avalon.framework.configuration.Configurable;
029import org.apache.avalon.framework.configuration.Configuration;
030import org.apache.avalon.framework.configuration.ConfigurationException;
031import org.apache.avalon.framework.context.ContextException;
032import org.apache.avalon.framework.context.Contextualizable;
033import org.apache.avalon.framework.service.ServiceException;
034import org.apache.avalon.framework.service.ServiceManager;
035import org.apache.avalon.framework.service.Serviceable;
036import org.apache.cocoon.Constants;
037import org.apache.cocoon.environment.Context;
038import org.apache.commons.io.IOUtils;
039import org.apache.commons.lang.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            @SuppressWarnings("resource")
204            InputStream mapperStream = null;
205            String mapperLocation = null;
206
207            try
208            {
209                if ("config".equals(sourceType))
210                {
211                    File file = null;
212                    if (source.startsWith("/"))
213                    {
214                        // Absolute path (from the root context path).
215                        file = new File(_contextPath, source);
216                    }
217                    else
218                    {
219                        // Relative path
220                        File pluginDir = PluginsManager.getInstance().getPluginLocation(_pluginName);
221                        file = new File(pluginDir, source);
222                    }
223                    
224                    mapperLocation = file.toURI().toASCIIString();
225                    try
226                    {
227                        mapperStream = new FileInputStream(file);
228                    }
229                    catch (FileNotFoundException e)
230                    {
231                        throw new RuntimeException("Cannot (re)load MyBatis: Cannot find configuration file: " + file, e);
232                    }
233                }
234                else
235                {
236                    mapperLocation = source;
237                    mapperStream = getClass().getResourceAsStream(source);
238                }
239                
240                if (getLogger().isInfoEnabled())
241                {
242                    getLogger().info("Initialized mybatis mapper at location '{}' for datasource id '{}'", mapperLocation, _dataSourceId);
243                }
244
245                XMLMapperBuilder mapperParser = new XMLMapperBuilder(mapperStream, config, mapperLocation, config.getSqlFragments());
246                mapperParser.parse();
247            }
248            catch (Exception e)
249            {
250                // Consider it needs a reload next time the method is called
251                _dataSourceId = null;
252                throw e;
253            }
254            finally
255            {
256                IOUtils.closeQuietly(mapperStream);
257            }
258        }
259
260        _sessionFactory = sessionFactoryBuilder.build(config);
261    }
262    
263    /**
264     * Get the mybatis configuration
265     * @param env the mybatis environnement
266     * @return the mybatis configuration
267     */
268    protected org.apache.ibatis.session.Configuration _getMyBatisConfiguration(Environment env)
269    {
270        org.apache.ibatis.session.Configuration config = new org.apache.ibatis.session.Configuration(env);
271        config.setCacheEnabled(true);
272        config.setLazyLoadingEnabled(true);
273        
274        return config;
275    }
276
277    /**
278     * Get datasource id
279     * @return the datasource id
280     */
281    protected String _getDataSourceId()
282    {
283        // Let's check if MyBatis current configuration is ok
284        String newDatasourceId;
285        if (_dataSourceConfigurationParameter)
286        {
287            newDatasourceId = Config.getInstance().getValueAsString(_dataSourceParameter);
288        }
289        else
290        {
291            newDatasourceId = _dataSourceParameter;
292        }
293        
294        if (getSQLDataSourceManager().getDefaultDataSourceId().equals(newDatasourceId))
295        {
296            // resolve "default", as default may change
297            newDatasourceId = getSQLDataSourceManager().getDefaultDataSourceDefinition().getId();
298        }
299        
300        return newDatasourceId;
301    }
302    
303    /**
304     * Returns the myBatis {@link SqlSession}.
305     * @return the myBatis {@link SqlSession}.
306     */
307    protected SqlSession getSession()
308    {
309        return getSession(false);
310    }
311    
312    /**
313     * Returns the myBatis {@link SqlSession}.
314     * @param autoCommit if the underlying Connection should auto commit statements.
315     * @return the myBatis {@link SqlSession}.
316     */
317    protected SqlSession getSession(boolean autoCommit)
318    {
319        reload();
320        return _sessionFactory.openSession(autoCommit);
321    }
322    
323    class SqlMap
324    {
325        private String _source;
326        private String _sourceType;
327        
328        public String getSource()
329        {
330            return _source;
331        }
332        
333        public void setSource(String source)
334        {
335            _source = source;
336        }
337        
338        public String getSourceType()
339        {
340            return _sourceType;
341        }
342        
343        public void setSourceType(String sourceType)
344        {
345            _sourceType = sourceType;
346        }
347    }
348}