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 _synchronizeMonitoringAndCaptcha(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 _synchronizeMonitoringAndCaptcha(Configuration boConfiguration) 323 { 324 // MONITORING 325 Config config = Config.getInstance(); 326 Configuration monitoringConfiguration = boConfiguration.getChild("Monitoring"); 327 boolean enabled = monitoringConfiguration.getAttributeAsBoolean("enabled", false); 328 boolean wasEnabled = config.getValue("front.cache.monitoring.schedulers.enable"); 329 String datasourceId = monitoringConfiguration.getChild("Datasource").getValue(""); 330 331 Map<String, Object> valuesToChange = new HashMap<>(); 332 if (enabled != wasEnabled) 333 { 334 valuesToChange.put("front.cache.monitoring.schedulers.enable", enabled); 335 } 336 if (enabled && !StringUtils.equals(datasourceId, config.getValue("front.cache.monitoring.datasource.jdbc.pool"))) 337 { 338 valuesToChange.put("front.cache.monitoring.datasource.jdbc.pool", datasourceId); 339 } 340 341 // CAPTCHA 342 Configuration captchaConfiguration = boConfiguration.getChild("Captcha"); 343 String captchaType = captchaConfiguration.getAttribute("type", null); 344 String captchaPublicKey = captchaConfiguration.getAttribute("publicKey", null); 345 String captchaSecretKey = captchaConfiguration.getAttribute("secretKey", null); 346 347 if (!StringUtils.equals(captchaType, config.getValue("runtime.captcha.type")) 348 || !StringUtils.equals(captchaPublicKey, config.getValue("runtime.captcha.recaptcha.publickey")) 349 || !StringUtils.equals(captchaSecretKey, config.getValue("runtime.captcha.recaptcha.secretkey"))) 350 { 351 valuesToChange.put("runtime.captcha.type", captchaType); 352 valuesToChange.put("runtime.captcha.recaptcha.publickey", captchaPublicKey); 353 valuesToChange.put("runtime.captcha.recaptcha.secretkey", captchaSecretKey); 354 } 355 356 // UPDATE CONFIG 357 if (!valuesToChange.isEmpty()) 358 { 359 try 360 { 361 Map<String, Object> existingValues = config.getValues(); 362 existingValues.putAll(valuesToChange); 363 ConfigManager.getInstance().save(existingValues, new File(AmetysHomeHelper.getAmetysHomeConfig(), RuntimeServlet.CONFIG_FILE_NAME).getCanonicalPath()); 364 // Need a restart to be taken in account 365 } 366 catch (Exception e) 367 { 368 getLogger().error("The monitoring/captcha synchronization failed", e); 369 } 370 } 371 } 372}