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}