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}