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    private SqlSessionFactory _sessionFactory;
060    private SQLDataSourceManager _sqlDataSourceManager;
061    private String _contextPath;
062    private String _pluginName;
063    
064    private String _dataSourceId;
065    
066    private String _dataSourceParameter;
067    private boolean _dataSourceConfigurationParameter;
068    private Set<SqlMap> _sqlMaps;
069    private ServiceManager _manager;
070    
071    public void contextualize(org.apache.avalon.framework.context.Context context) throws ContextException
072    {
073        Context ctx = (Context) context.get(Constants.CONTEXT_ENVIRONMENT_CONTEXT);
074        _contextPath = ctx.getRealPath("/");
075    }
076
077    @Override
078    public void service(ServiceManager manager) throws ServiceException
079    {
080        _manager = manager;
081    }
082    
083    private SQLDataSourceManager getSQLDataSourceManager()
084    {
085        if (_sqlDataSourceManager == null)
086        {
087            try
088            {
089                _sqlDataSourceManager = (SQLDataSourceManager) _manager.lookup(SQLDataSourceManager.ROLE);
090            }
091            catch (ServiceException e)
092            {
093                throw new RuntimeException(e);
094            }
095        }
096        return _sqlDataSourceManager;
097    }
098    
099    public void setPluginInfo(String pluginName, String featureName, String id)
100    {
101        _pluginName = pluginName;
102    }
103    
104    public void configure(Configuration configuration) throws ConfigurationException
105    {
106        Configuration dataSourceConf = configuration.getChild("datasource", false);
107        if (dataSourceConf == null)
108        {
109            throw new ConfigurationException("The 'datasource' configuration node must be defined.", dataSourceConf);
110        }
111        
112        String dataSourceConfParam = dataSourceConf.getValue();
113        String dataSourceConfType = dataSourceConf.getAttribute("type", "config");
114        
115        _dataSourceConfigurationParameter = StringUtils.equals(dataSourceConfType, "config");
116        _dataSourceParameter = dataSourceConfParam;
117        
118        _sqlMaps = new HashSet<>();
119        Configuration[] sqlMaps = configuration.getChildren("sqlMap");
120        for (Configuration sqlMapConf : sqlMaps)
121        {
122            String resourceSrc = sqlMapConf.getAttribute("resource", null);
123            String configSrc = sqlMapConf.getAttribute("config", null);
124            
125            if (StringUtils.isBlank(resourceSrc) && StringUtils.isBlank(configSrc))
126            {
127                throw new ConfigurationException("The sqlmap configuration must have a 'resource' or 'config' attribute.", sqlMapConf);
128            }
129            if (StringUtils.isNotBlank(resourceSrc) && StringUtils.isNotBlank(configSrc))
130            {
131                throw new ConfigurationException("The sqlmap configuration can't have both 'resource' and 'config' attributes.", sqlMapConf);
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     * Reload configuration and object for mybatis
153     */
154    protected synchronized void reload()
155    {
156        // Let's check if MyBatis current configuration is ok
157        String newDatasourceId;
158        if (_dataSourceConfigurationParameter)
159        {
160            newDatasourceId = Config.getInstance().getValueAsString(_dataSourceParameter);
161        }
162        else
163        {
164            newDatasourceId = _dataSourceParameter;
165        }
166        
167        if (getSQLDataSourceManager().getDefaultDataSourceId().equals(newDatasourceId))
168        {
169            // resolve "default", as default may change
170            newDatasourceId = getSQLDataSourceManager().getDefaultDataSourceDefinition().getId();
171        }
172        
173        if (StringUtils.equals(newDatasourceId, _dataSourceId))
174        {
175            return;
176        }
177        
178        // No it's not ok. Let's reload
179        _dataSourceId = newDatasourceId;
180        
181        DataSource dataSource = getSQLDataSourceManager().getSQLDataSource(_dataSourceId);
182        if (dataSource == null)
183        {
184            throw new RuntimeException("Cannot (re)load MyBatis: Invalid datasource id: " + _dataSourceId);
185        }
186        
187        SqlSessionFactoryBuilder sessionFactoryBuilder = new SqlSessionFactoryBuilder();
188        
189        TransactionFactory transactionFactory = new JdbcTransactionFactory();
190        Environment env = new Environment(_dataSourceId, transactionFactory, dataSource);
191        
192        org.apache.ibatis.session.Configuration config = new org.apache.ibatis.session.Configuration(env);
193        config.setCacheEnabled(true);
194        config.setLazyLoadingEnabled(true);
195        
196        for (SqlMap sqlMap : _sqlMaps)
197        {
198            String sourceType = sqlMap.getSourceType();
199            String source = sqlMap.getSource();
200        
201            @SuppressWarnings("resource")
202            InputStream mapperStream = null;
203            String mapperLocation = null;
204
205            try
206            {
207                if ("config".equals(sourceType))
208                {
209                    File file = null;
210                    if (source.startsWith("/"))
211                    {
212                        // Absolute path (from the root context path).
213                        file = new File(_contextPath, source);
214                    }
215                    else
216                    {
217                        // Relative path
218                        File pluginDir = PluginsManager.getInstance().getPluginLocation(_pluginName);
219                        file = new File(pluginDir, source);
220                    }
221                    
222                    mapperLocation = file.toURI().toASCIIString();
223                    try
224                    {
225                        mapperStream = new FileInputStream(file);
226                    }
227                    catch (FileNotFoundException e)
228                    {
229                        throw new RuntimeException("Cannot (re)load MyBatis: Cannot find configuration file: " + file, e);
230                    }
231                }
232                else
233                {
234                    mapperLocation = source;
235                    mapperStream = getClass().getResourceAsStream(source);
236                }
237                
238                if (getLogger().isInfoEnabled())
239                {
240                    getLogger().info("Initialized mybatis mapper at location '{}' for datasource id '{}'", mapperLocation, _dataSourceId);
241                }
242
243                XMLMapperBuilder mapperParser = new XMLMapperBuilder(mapperStream, config, mapperLocation, config.getSqlFragments());
244                mapperParser.parse();
245            }
246            finally
247            {
248                IOUtils.closeQuietly(mapperStream);
249            }
250        }
251
252        _sessionFactory = sessionFactoryBuilder.build(config);
253    }
254    
255    /**
256     * Returns the myBatis {@link SqlSession}.
257     * @return the myBatis {@link SqlSession}.
258     */
259    protected SqlSession getSession()
260    {
261        return getSession(false);
262    }
263    
264    /**
265     * Returns the myBatis {@link SqlSession}.
266     * @param autoCommit if the underlying Connection should auto commit statements.
267     * @return the myBatis {@link SqlSession}.
268     */
269    protected SqlSession getSession(boolean autoCommit)
270    {
271        reload();
272        return _sessionFactory.openSession(autoCommit);
273    }
274    
275    class SqlMap
276    {
277        private String _source;
278        private String _sourceType;
279        
280        public String getSource()
281        {
282            return _source;
283        }
284        
285        public void setSource(String source)
286        {
287            _source = source;
288        }
289        
290        public String getSourceType()
291        {
292            return _sourceType;
293        }
294        
295        public void setSourceType(String sourceType)
296        {
297            _sourceType = sourceType;
298        }
299    }
300}