001/*
002 *  Copyright 2017 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.plugins.site;
017
018import java.io.ByteArrayInputStream;
019import java.io.ByteArrayOutputStream;
020import java.io.File;
021import java.io.FileOutputStream;
022import java.io.OutputStream;
023import java.util.ArrayList;
024import java.util.Collection;
025import java.util.Comparator;
026import java.util.HashMap;
027import java.util.List;
028import java.util.Map;
029import java.util.Properties;
030import java.util.TreeMap;
031import java.util.concurrent.locks.Lock;
032import java.util.concurrent.locks.ReentrantLock;
033
034import javax.xml.transform.OutputKeys;
035import javax.xml.transform.TransformerFactory;
036import javax.xml.transform.sax.SAXTransformerFactory;
037import javax.xml.transform.sax.TransformerHandler;
038import javax.xml.transform.stream.StreamResult;
039
040import org.apache.avalon.framework.component.Component;
041import org.apache.avalon.framework.configuration.Configuration;
042import org.apache.avalon.framework.configuration.ConfigurationException;
043import org.apache.avalon.framework.configuration.DefaultConfigurationBuilder;
044import org.apache.avalon.framework.configuration.DefaultConfigurationSerializer;
045import org.apache.avalon.framework.service.ServiceException;
046import org.apache.avalon.framework.service.ServiceManager;
047import org.apache.avalon.framework.service.Serviceable;
048import org.apache.commons.lang.StringUtils;
049import org.apache.http.client.methods.CloseableHttpResponse;
050import org.apache.http.client.methods.HttpGet;
051import org.apache.http.impl.client.CloseableHttpClient;
052import org.apache.xml.serializer.OutputPropertiesFactory;
053
054import org.ametys.core.datasource.LDAPDataSourceManager;
055import org.ametys.core.datasource.SQLDataSourceManager;
056import org.ametys.core.user.population.UserPopulationDAO;
057import org.ametys.runtime.config.Config;
058import org.ametys.runtime.config.ConfigManager;
059import org.ametys.runtime.plugin.component.AbstractLogEnabled;
060import org.ametys.runtime.servlet.RuntimeServlet;
061import org.ametys.runtime.util.AmetysHomeHelper;
062import org.ametys.site.BackOfficeRequestHelper;
063
064/**
065 * A cache for site information provided by the Back-Office.
066 */
067public class SiteInformationCache extends AbstractLogEnabled implements Serviceable, Component
068{
069    /** Avalon Role */ 
070    public static final String ROLE = SiteInformationCache.class.getName();
071    
072    /** Prefix for backoffice synchronized userpopulations */
073    public static final String BACKOFFICE_PREFIX_IDENTIFIER = "bo-";
074    
075    private final Lock _syncLock = new ReentrantLock();
076    
077    private Map<SiteUrl, Site> _sites;
078    
079    private LDAPDataSourceManager _ldapDataSourceManager;
080    private SQLDataSourceManager _sqlDataSourceManager;
081    private UserPopulationDAO _userPopulationDAO;
082
083    public void service(ServiceManager manager) throws ServiceException
084    {
085        _sqlDataSourceManager = (SQLDataSourceManager) manager.lookup(SQLDataSourceManager.ROLE);
086        _ldapDataSourceManager = (LDAPDataSourceManager) manager.lookup(LDAPDataSourceManager.ROLE);
087        _userPopulationDAO = (UserPopulationDAO) manager.lookup(UserPopulationDAO.ROLE);
088    }
089    
090    /**
091     * Clear cached informations.
092     */
093    public void resetSitesCache()
094    {
095        _sites = null;
096    }
097    
098    /**
099     * Returns the cached informations.
100     * @return the cached informations.
101     */
102    public Map<SiteUrl, Site> getSites()
103    {
104        _synchronize();
105        return _sites;
106    }
107
108    private void _synchronize()
109    {
110        if (_sites == null)
111        {
112            if (_syncLock.tryLock())
113            {
114                try
115                {
116                    _synchronizeSites();
117                    _synchronizePopulationsAndDatasources();
118                }
119                catch (Exception e)
120                {
121                    throw new RuntimeException("Unable to synchronize sites data", e);
122                }
123                finally
124                {
125                    _syncLock.unlock();
126                }
127            }
128            else
129            {
130                // some is already filling _sites for us... we just have to wait
131                _syncLock.lock();
132                _syncLock.unlock();
133            }
134        }
135    }
136    
137    private void _synchronizeSites() throws ConfigurationException
138    {
139        Collection<Site> sites = new ArrayList<>();
140        
141        Configuration boConfiguration = _getBackofficeConfiguration("/_sites.xml");
142        _configureSites (boConfiguration, sites);
143        
144        TreeMap<SiteUrl, Site> sortedSites = new TreeMap<>(new SiteUrlComparator());
145        
146        for (Site site : sites)
147        {
148            for (SiteUrl url : site.getSiteUrls())
149            {
150                sortedSites.put(url, site);
151            }
152        }
153        
154        _sites = sortedSites;
155    }
156    
157    private void _configureSites (Configuration conf, Collection<Site> sites) throws ConfigurationException
158    {
159        Configuration[] sitesConf = conf.getChildren("site");
160        for (Configuration siteConf : sitesConf)
161        {
162            String name = siteConf.getAttribute("name");
163
164            List<String> populationIds = new ArrayList<>();
165            for (Configuration populationConf : siteConf.getChild("populations").getChildren())
166            {
167                populationIds.add(populationConf.getValue());
168            }
169            
170            List<SiteUrl> urls = new ArrayList<>();
171            for (Configuration urlConf : siteConf.getChildren("url"))
172            {
173                String serverName = urlConf.getAttribute("serverName");
174                String serverPort = urlConf.getAttribute("serverPort");
175                String serverPath = urlConf.getAttribute("serverPath");
176                
177                urls.add(new SiteUrl(serverName, serverPort, serverPath));
178            }
179    
180            List<String> languages = new ArrayList<>();
181            for (Configuration langConf : siteConf.getChild("languages").getChildren())
182            {
183                languages.add(langConf.getName());
184            }
185            
186            sites.add(new Site(name, urls, languages, populationIds));
187            
188            // Sub sites
189            _configureSites (siteConf, sites);
190        }
191    }
192    
193    class SiteUrlComparator implements Comparator<SiteUrl>
194    {
195        @Override
196        public int compare(SiteUrl url1, SiteUrl url2)
197        {
198            int result = url2.getServerName().compareTo(url1.getServerName());
199            
200            if (result != 0)
201            {
202                return result;
203            }
204            
205            result = url2.getServerPort().compareTo(url1.getServerPort());
206            
207            if (result != 0)
208            {
209                return result;
210            }
211            
212            return url2.getServerPath().compareTo(url1.getServerPath());
213        }
214    }
215    
216    private Configuration _getBackofficeConfiguration(String url)
217    {
218        // Get site names and URLs from the CMS
219        String cmsURL = Config.getInstance().getValue("org.ametys.site.bo");
220        
221        try (CloseableHttpClient httpClient = BackOfficeRequestHelper.getHttpClient())
222        {
223            HttpGet httpGet = new HttpGet(cmsURL + url);
224            httpGet.addHeader("X-Ametys-FO", "true");    
225            
226            try (CloseableHttpResponse response = httpClient.execute(httpGet); ByteArrayOutputStream os = new ByteArrayOutputStream())
227            {
228                switch (response.getStatusLine().getStatusCode())
229                {
230                    case 200: 
231                        break;
232                    
233                    case 403: 
234                        throw new IllegalStateException("The CMS back-office refused the connection");
235                        
236                    case 500: 
237                    default:
238                        throw new IllegalStateException("The CMS back-office returned an error");
239                }
240                
241                response.getEntity().writeTo(os);
242                try (ByteArrayInputStream is = new ByteArrayInputStream(os.toByteArray()))
243                {
244                    Configuration conf = new DefaultConfigurationBuilder().build(is);
245                    return conf;
246                }
247            }
248        }
249        catch (Exception e)
250        {
251            throw new RuntimeException("Unable to synchronize site data", e);
252        }
253    }
254    
255    /**
256     * Synchronize the local user populations and datasources with backoffice
257     * @throws Exception If an unexpected error occurred
258     */
259    private void _synchronizePopulationsAndDatasources() throws Exception
260    {
261        Configuration configuration = _getBackofficeConfiguration("/_sites-populations.xml");
262        
263        // First, we need to remove the files because they will be read before we reinitialize them by the DataSourceConsumerEP
264        _userPopulationDAO.getConfigurationFile().delete();
265        _sqlDataSourceManager.getFileConfiguration().delete();
266        _ldapDataSourceManager.getFileConfiguration().delete();
267
268        // Then we can destroy what is in memory
269        _userPopulationDAO.dispose();
270        _sqlDataSourceManager.dispose(true);
271        _ldapDataSourceManager.dispose();
272        
273        // And finally we can transfer data
274        
275        // SQL datasources
276        Configuration sqlDatasources = configuration.getChild("SQLDatasources").getChild("datasources");
277        
278        _serialize(sqlDatasources, _sqlDataSourceManager.getFileConfiguration());
279        _sqlDataSourceManager.initialize(true);
280        
281        // LDAP Datasources
282        _serialize(configuration.getChild("LDAPDatasources").getChild("datasources"), _ldapDataSourceManager.getFileConfiguration());
283        _ldapDataSourceManager.initialize();
284        
285        // User populations
286        _serialize(configuration.getChild("UserPopulations").getChild("userPopulations"), _userPopulationDAO.getConfigurationFile());
287        _userPopulationDAO.initialize();
288        
289        // Config values
290        _synchronizeConfigValues(configuration);
291    }
292    
293    private void _serialize(Configuration configuration, File file) throws Exception
294    {
295        if (!file.exists())
296        {
297            file.getParentFile().mkdirs();
298            file.createNewFile();
299        }
300        
301        // create a transformer for saving sax into a file
302        TransformerHandler th = ((SAXTransformerFactory) TransformerFactory.newInstance()).newTransformerHandler();
303
304        // create the result where to write
305        try (OutputStream os = new FileOutputStream(file))
306        {
307            StreamResult sResult = new StreamResult(os);
308            th.setResult(sResult);
309
310            // create the format of result
311            Properties format = new Properties();
312            format.put(OutputKeys.METHOD, "xml");
313            format.put(OutputKeys.INDENT, "yes");
314            format.put(OutputKeys.ENCODING, "UTF-8");
315            format.put(OutputPropertiesFactory.S_KEY_INDENT_AMOUNT, "4");
316            th.getTransformer().setOutputProperties(format);
317
318            new DefaultConfigurationSerializer().serialize(th, configuration);
319        }
320    }
321    
322    private void _synchronizeConfigValues(Configuration boConfiguration)
323    {
324        Map<String, Object> valuesToChange = new HashMap<>();
325        
326        _synchronizeMonitory(boConfiguration, valuesToChange);
327        _synchronizeCaptcha(boConfiguration, valuesToChange);
328        _synchronizeUpload(boConfiguration, valuesToChange);
329        
330        // UPDATE CONFIG
331        if (!valuesToChange.isEmpty())
332        {
333            try
334            {
335                Map<String, Object> existingValues = Config.getInstance().getValues();
336                existingValues.putAll(valuesToChange);
337                ConfigManager.getInstance().save(existingValues, new File(AmetysHomeHelper.getAmetysHomeConfig(), RuntimeServlet.CONFIG_FILE_NAME).getCanonicalPath());
338                // Need a restart to be taken in account 
339            }
340            catch (Exception e)
341            {
342                getLogger().error("The monitoring/captcha synchronization failed", e);
343            }
344        }
345    }
346
347    private void _synchronizeUpload(Configuration boConfiguration, Map<String, Object> valuesToChange)
348    {
349        Configuration uploadConfiguration = boConfiguration.getChild("UploadMaxSize");
350        Long uploadMaxSize = uploadConfiguration.getValueAsLong(-1);
351        
352        if (uploadMaxSize != Config.getInstance().getValue("runtime.upload.max-size")) 
353        {
354            valuesToChange.put("runtime.upload.max-size", uploadMaxSize);
355        }
356    }
357
358    private void _synchronizeCaptcha(Configuration boConfiguration, Map<String, Object> valuesToChange)
359    {
360        Configuration captchaConfiguration = boConfiguration.getChild("Captcha");
361        String captchaType = captchaConfiguration.getAttribute("type", null);
362        String captchaPublicKey = captchaConfiguration.getAttribute("publicKey", null);
363        String captchaSecretKey = captchaConfiguration.getAttribute("secretKey", null);
364        
365        if (!StringUtils.equals(captchaType, Config.getInstance().getValue("runtime.captcha.type"))
366                || !StringUtils.equals(captchaPublicKey, Config.getInstance().getValue("runtime.captcha.recaptcha.publickey"))
367                || !StringUtils.equals(captchaSecretKey, Config.getInstance().getValue("runtime.captcha.recaptcha.secretkey")))
368        {
369            valuesToChange.put("runtime.captcha.type", captchaType);
370            valuesToChange.put("runtime.captcha.recaptcha.publickey", captchaPublicKey);
371            valuesToChange.put("runtime.captcha.recaptcha.secretkey", captchaSecretKey);
372        }
373    }
374
375    private void _synchronizeMonitory(Configuration boConfiguration, Map<String, Object> valuesToChange)
376    {
377        Configuration monitoringConfiguration = boConfiguration.getChild("Monitoring");
378        boolean enabled = monitoringConfiguration.getAttributeAsBoolean("enabled", false);
379        boolean wasEnabled = Config.getInstance().getValue("front.cache.monitoring.schedulers.enable");
380        String datasourceId = monitoringConfiguration.getChild("Datasource").getValue("");
381        
382        if (enabled != wasEnabled)
383        {
384            valuesToChange.put("front.cache.monitoring.schedulers.enable", enabled);
385        }
386        if (enabled && !StringUtils.equals(datasourceId, Config.getInstance().getValue("front.cache.monitoring.datasource.jdbc.pool")))
387        {
388            valuesToChange.put("front.cache.monitoring.datasource.jdbc.pool", datasourceId);
389        }
390        
391    }
392}