001/* 002 * Copyright 2023 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.runtime.plugins.admin.statistics; 017 018import java.io.IOException; 019import java.nio.charset.StandardCharsets; 020import java.util.Map; 021import java.util.Set; 022 023import org.apache.avalon.framework.activity.Disposable; 024import org.apache.avalon.framework.activity.Initializable; 025import org.apache.avalon.framework.service.ServiceException; 026import org.apache.avalon.framework.service.ServiceManager; 027import org.apache.hc.client5.http.classic.methods.HttpPost; 028import org.apache.hc.client5.http.entity.UrlEncodedFormEntity; 029import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; 030import org.apache.hc.core5.http.io.entity.EntityUtils; 031import org.apache.hc.core5.http.message.BasicNameValuePair; 032import org.apache.hc.core5.io.CloseMode; 033import org.quartz.JobExecutionContext; 034 035import org.ametys.core.schedule.progression.ContainerProgressionTracker; 036import org.ametys.core.util.HttpUtils; 037import org.ametys.core.util.JSONUtils; 038import org.ametys.plugins.core.impl.schedule.AbstractStaticSchedulable; 039import org.ametys.runtime.config.Config; 040import org.ametys.runtime.servlet.RuntimeServlet; 041 042/** 043 * Compute and optionally sent the anonymous statistics to the central ametys.org server 044 */ 045public class StatisticsSchedulable extends AbstractStaticSchedulable implements Initializable, Disposable 046{ 047 private static final String CENTRAL_SERVER_URL = "https://statistics.ametys.org/_update-version/statistics/1.0.0/upload.json"; 048 private static final String CENTRAL_SERVER_HEADER = "X-Ametys-Statistics"; 049 050 private JSONUtils _jsonUtils; 051 private StatisticsProviderExtensionPoint _statisticsExtensionPoint; 052 053 private CloseableHttpClient _httpClient; 054 055 @Override 056 public void service(ServiceManager manager) throws ServiceException 057 { 058 super.service(manager); 059 _jsonUtils = (JSONUtils) manager.lookup(JSONUtils.ROLE); 060 _statisticsExtensionPoint = (StatisticsProviderExtensionPoint) manager.lookup(StatisticsProviderExtensionPoint.ROLE); 061 } 062 063 public void initialize() throws Exception 064 { 065 _httpClient = HttpUtils.createHttpClient(0, 30); 066 } 067 068 @Override 069 public void execute(JobExecutionContext context, ContainerProgressionTracker progressionTracker) throws Exception 070 { 071 getLogger().info("Preparing statistics"); 072 Map<String, Object> jsonStatistics = _statisticsExtensionPoint.computeStatistics(); 073 074 if (Config.getInstance().getValue("runtime.statistics.send-at-night", false, false)) 075 { 076 getLogger().info("Sending remote statistics"); 077 _sendReport(jsonStatistics); 078 } 079 } 080 081 private void _sendReport(Map<String, Object> report) 082 { 083 try 084 { 085 // Prepare a request object 086 HttpPost request = new HttpPost(CENTRAL_SERVER_URL); 087 request.addHeader(CENTRAL_SERVER_HEADER, RuntimeServlet.getInstanceId()); 088 request.setEntity(new UrlEncodedFormEntity(Set.of(new BasicNameValuePair("value", _jsonUtils.convertObjectToJson(report))), StandardCharsets.UTF_8)); 089 090 // Execute the request 091 _httpClient.execute(request, response -> { 092 if (response.getCode() != 200) 093 { 094 throw new IllegalStateException("Could not join the central ametys.org server at " + CENTRAL_SERVER_URL + ". Error code " + response.getCode()); 095 } 096 else if (!response.containsHeader(CENTRAL_SERVER_HEADER)) 097 { 098 throw new IllegalStateException("Could not join the central ametys.org server at " + CENTRAL_SERVER_URL + ". Response code is 200, but there is not " + CENTRAL_SERVER_HEADER + " header"); 099 } 100 101 try 102 { 103 String responseAsString = EntityUtils.toString(response.getEntity(), "UTF-8"); 104 Map<String, Object> convertJsonToMap = _jsonUtils.convertJsonToMap(responseAsString); 105 106 if (!(convertJsonToMap.get("success") instanceof Boolean b && b == Boolean.TRUE)) 107 { 108 throw new IllegalStateException("Joined the central ametys.org server at " + CENTRAL_SERVER_URL + ". But the operation failed."); 109 } 110 } 111 catch (IllegalArgumentException e) 112 { 113 throw new IllegalStateException("Joined the central ametys.org server at " + CENTRAL_SERVER_URL + ". But cannot parse the response.", e); 114 } 115 116 return true; 117 }); 118 } 119 catch (IOException e) 120 { 121 throw new IllegalStateException("Could not join the central ametys.org server at " + CENTRAL_SERVER_URL, e); 122 } 123 } 124 125 public void dispose() 126 { 127 _httpClient.close(CloseMode.GRACEFUL); 128 } 129}