View Javadoc
1   /*
2    * Licensed to The Apereo Foundation under one or more contributor license
3    * agreements. See the NOTICE file distributed with this work for additional
4    * information regarding copyright ownership.
5    *
6    *
7    * The Apereo Foundation licenses this file to you under the Educational
8    * Community License, Version 2.0 (the "License"); you may not use this file
9    * except in compliance with the License. You may obtain a copy of the License
10   * at:
11   *
12   *   http://opensource.org/licenses/ecl2.txt
13   *
14   * Unless required by applicable law or agreed to in writing, software
15   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
16   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
17   * License for the specific language governing permissions and limitations under
18   * the License.
19   *
20   */
21  
22  package org.opencastproject.adopter.registration;
23  
24  import static org.opencastproject.db.Queries.namedQuery;
25  
26  import org.opencastproject.adopter.registration.dto.Adopter;
27  import org.opencastproject.adopter.registration.dto.GeneralData;
28  import org.opencastproject.adopter.registration.dto.Host;
29  import org.opencastproject.adopter.registration.dto.StatisticData;
30  import org.opencastproject.assetmanager.api.AssetManager;
31  import org.opencastproject.capture.admin.api.CaptureAgentStateService;
32  import org.opencastproject.db.DBSession;
33  import org.opencastproject.db.DBSessionFactory;
34  import org.opencastproject.metadata.dublincore.DublinCore;
35  import org.opencastproject.metadata.dublincore.EncodingSchemeUtils;
36  import org.opencastproject.search.api.SearchResult;
37  import org.opencastproject.search.api.SearchResultList;
38  import org.opencastproject.search.api.SearchService;
39  import org.opencastproject.security.api.DefaultOrganization;
40  import org.opencastproject.security.api.Organization;
41  import org.opencastproject.security.api.OrganizationDirectoryService;
42  import org.opencastproject.security.api.SecurityService;
43  import org.opencastproject.security.api.User;
44  import org.opencastproject.security.api.UserProvider;
45  import org.opencastproject.security.util.SecurityUtil;
46  import org.opencastproject.series.api.SeriesService;
47  import org.opencastproject.serviceregistry.api.ServiceRegistry;
48  import org.opencastproject.serviceregistry.api.ServiceRegistryException;
49  import org.opencastproject.userdirectory.JpaUserAndRoleProvider;
50  import org.opencastproject.userdirectory.JpaUserReferenceProvider;
51  
52  import com.google.gson.Gson;
53  
54  import org.elasticsearch.index.query.QueryBuilders;
55  import org.elasticsearch.search.builder.SearchSourceBuilder;
56  import org.osgi.framework.BundleContext;
57  import org.osgi.framework.Version;
58  import org.osgi.service.component.annotations.Activate;
59  import org.osgi.service.component.annotations.Component;
60  import org.osgi.service.component.annotations.Deactivate;
61  import org.osgi.service.component.annotations.Reference;
62  import org.slf4j.Logger;
63  import org.slf4j.LoggerFactory;
64  
65  import java.io.IOException;
66  import java.util.Date;
67  import java.util.LinkedHashMap;
68  import java.util.List;
69  import java.util.Map;
70  import java.util.Objects;
71  import java.util.Optional;
72  import java.util.Timer;
73  import java.util.TimerTask;
74  import java.util.UUID;
75  import java.util.stream.Collectors;
76  
77  import javax.persistence.EntityManagerFactory;
78  import javax.persistence.TypedQuery;
79  
80  /**
81   * It collects and sends statistic data of an registered adopter.
82   */
83  @Component(
84      immediate = true,
85      service = AdopterRegistrationServiceImpl.class,
86      property = {
87          "service.description=Adopter Statistics Registration Service"
88      }
89  )
90  public class AdopterRegistrationServiceImpl extends TimerTask {
91  
92    /** The logger */
93    private static final Logger logger = LoggerFactory.getLogger(AdopterRegistrationServiceImpl.class);
94  
95    /** The property key containing the address of the external server where the statistic data will be send to. */
96    private static final String PROP_KEY_STATISTIC_SERVER_ADDRESS = "org.opencastproject.adopter.registration.server.url";
97    private static final String DEFAULT_STATISTIC_SERVER_ADDRESS = "https://register.opencast.org";
98  
99    private static final int ONE_DAY_IN_MILLISECONDS = 1000 * 60 * 60 * 24;
100 
101   private static final Gson gson = new Gson();
102 
103   //================================================================================
104   // OSGi properties
105   //================================================================================
106 
107   /** Provides access to job and host information */
108   private ServiceRegistry serviceRegistry;
109 
110   /** Provides access to CA counts */
111   private CaptureAgentStateService caStateService;
112 
113   private OrganizationDirectoryService organizationDirectoryService;
114 
115   /** Provides access to recording information */
116   private AssetManager assetManager;
117 
118   /** Provides access to series information */
119   private SeriesService seriesService;
120 
121   /** Provides access to search information */
122   private SearchService searchService;
123 
124   /** User and role provider */
125   protected UserProvider userRefProvider;
126 
127   protected JpaUserAndRoleProvider userProvider;
128 
129   /** The security service */
130   protected SecurityService securityService;
131 
132   /** The factory for creating the entity manager. */
133   protected EntityManagerFactory emf = null;
134 
135   protected DBSessionFactory dbSessionFactory;
136 
137   protected DBSession db;
138 
139   /** OSGi setter for the entity manager factory. */
140   @Reference(target = "(osgi.unit.name=org.opencastproject.adopter.impl)")
141   public void setEntityManagerFactory(EntityManagerFactory emf) {
142     this.emf = emf;
143   }
144 
145   @Reference
146   public void setDBSessionFactory(DBSessionFactory dbSessionFactory) {
147     this.dbSessionFactory = dbSessionFactory;
148   }
149 
150 
151   //================================================================================
152   // Properties
153   //================================================================================
154 
155   /** Provides methods for sending statistic data */
156   private AdopterRegistrationSender sender;
157 
158   /** The organisation of the system admin user */
159   private Organization defaultOrganization;
160 
161   /** System admin user */
162   private User systemAdminUser;
163 
164   /** The Opencast version this is running in */
165   private String version;
166 
167   /** The timer for shutdown uses */
168   private Timer timer;
169 
170   //================================================================================
171   // Scheduler methods
172   //================================================================================
173 
174   /**
175    * Entry point of the scheduler. Configured with the
176    * activate parameter at OSGi component declaration.
177    * @param ctx OSGi component context
178    */
179   @Activate
180   public void activate(BundleContext ctx) {
181     logger.info("Activating adopter registration service.");
182     this.defaultOrganization = new DefaultOrganization();
183     String systemAdminUserName = ctx.getProperty(SecurityUtil.PROPERTY_KEY_SYS_USER);
184     this.systemAdminUser = SecurityUtil.createSystemUser(systemAdminUserName, defaultOrganization);
185 
186     final Version ctxVersion = ctx.getBundle().getVersion();
187     this.version = ctxVersion.toString();
188 
189     // We read this key for testing but don't ever expect this to be set.
190     final String serverBaseUrl = ctx.getProperty(PROP_KEY_STATISTIC_SERVER_ADDRESS);
191     if (serverBaseUrl != null) {
192       logger.error("\nAdopter registration information are sent to a server other than register.opencast.org.\n"
193           + "We cannot take any responsibility for what is done with the data.");
194     }
195 
196     db = dbSessionFactory.createSession(emf);
197 
198     this.sender = new AdopterRegistrationSender(Objects.toString(serverBaseUrl, DEFAULT_STATISTIC_SERVER_ADDRESS));
199 
200     // Send data now. Repeat every 24h.
201     timer = new Timer();
202     timer.schedule(this, 0, ONE_DAY_IN_MILLISECONDS);
203   }
204 
205   @Deactivate
206   public void deactivate() {
207     timer.cancel();
208     db.close();
209   }
210 
211   /**
212    * Saves the submitted registration form.
213    * @param adopter The adopter registration form.
214    */
215   public void save(Adopter adopter) throws AdopterRegistrationException {
216     try {
217       db.execTx(em -> {
218         Optional<Adopter> dbForm = namedQuery.findOpt("Adopter.findAll", Adopter.class).apply(em);
219         if (dbForm.isEmpty()) {
220           // Null means, that there is no entry in the DB yet, so we create UUIDs for the keys.
221           adopter.setAdopterKey(UUID.randomUUID().toString());
222           adopter.setStatisticKey(UUID.randomUUID().toString());
223           adopter.setDateCreated(new Date());
224           adopter.setDateModified(new Date());
225           em.persist(adopter);
226         } else {
227           dbForm.get().merge(adopter);
228           em.merge(dbForm.get());
229         }
230 
231       });
232     } catch (Exception e) {
233       logger.error("Couldn't update the adopter statistics registration adopter: {}", e.getMessage());
234       throw new AdopterRegistrationException(e);
235     }
236   }
237 
238   public void markForDeletion() {
239     Adopter a = get();
240     if (null != a) {
241       a.delete();
242       save(a);
243     }
244   }
245 
246   public void delete() {
247     try {
248       db.execTx(namedQuery.delete("Adopter.deleteAll"));
249     } catch (Exception e) {
250       logger.error("Error occurred while deleting the adopter registration table. {}", e.getMessage());
251       throw new RuntimeException(e);
252     }
253   }
254 
255   public Adopter get() throws AdopterRegistrationException {
256     return db.exec(namedQuery.findOpt("Adopter.findAll", Adopter.class)).orElse(null);
257   }
258 
259   /**
260    * The scheduled method. It collects statistic data
261    * around Opencast and sends it via POST request.
262    */
263   @Override
264   public void run() {
265     logger.info("Executing adopter statistic scheduler task.");
266 
267     Adopter adopter;
268     try {
269       adopter = get();
270       if (null == adopter) {
271         logger.info("Adopter not registered, aborting");
272         return;
273       }
274     } catch (Exception e) {
275       logger.error("Couldn't retrieve adopter registration data.", e);
276       return;
277     }
278 
279     if (adopter.shouldDelete()) {
280       //Sanitize the data we're sending to delete things
281       Adopter f = new Adopter();
282       f.setAdopterKey(adopter.getAdopterKey());
283       GeneralData gd = new GeneralData(f);
284       gd.setAdopterKey(adopter.getAdopterKey());
285       StatisticData sd = new StatisticData(adopter.getStatisticKey());
286 
287       try {
288         sender.deleteStatistics(sd.jsonify());
289         sender.deleteGeneralData(gd.jsonify());
290         markForDeletion();
291       } catch (IOException e) {
292         logger.warn("Error occurred while deleting registration data, will retry", e);
293       }
294       return;
295     }
296     // Don't send data unless they've agreed to the latest (at time of writing) terms.
297     // Pre April 2022 doesn't allow collection of a bunch of things, and doens't allow linking stat data to org
298     // so rather than burning time turning various things off (after figuring out what needs to be turned off)
299     // we just don't send anything.  By the time we need to update the ToU again this whole thing would need reworking
300     // anyway, so we'll run with this for now.
301     if (adopter.isRegistered() && adopter.getTermsVersionAgreed() == Adopter.TERMSOFUSEVERSION.APRIL_2022) {
302       try {
303         String generalDataAsJson = collectGeneralData(adopter);
304         sender.sendGeneralData(generalDataAsJson);
305         //Note: save the form (unmodified) to update the dates.  Old dates cause warnings to the user!
306         save(adopter);
307       } catch (IOException e) {
308         logger.warn("Error occurred while processing adopter general data.", e);
309       }
310 
311       if (adopter.allowsStatistics()) {
312         try {
313           StatisticData statisticData = collectStatisticData(adopter.getAdopterKey(), adopter.getStatisticKey());
314           sender.sendStatistics(statisticData.jsonify());
315           db.exec(em -> {
316             TypedQuery<AdopterRegistrationExtra> q = em.createNamedQuery("AdopterRegistrationExtra.findAll",
317                 AdopterRegistrationExtra.class);
318             q.getResultList().forEach(extra -> {
319               try {
320                 Map<String, Object> data = Map.of("statistic_key", statisticData.getStatisticKey(),
321                     "data", gson.fromJson(extra.getData(), Map.class));
322                 sender.sendExtraData(extra.getType(), gson.toJson(data));
323               } catch (IOException e) {
324                 logger.warn("Unable to send extra adopter data with type '{}'", extra.getType(), e);
325               }
326             });
327           });
328           //Note: save the form (unmodified) (again!) to update the dates.  Old dates cause warnings to the user!
329           save(adopter);
330         } catch (IOException e) {
331           logger.warn("Unable to send adopter statistic data");
332         } catch (Exception e) {
333           logger.error("Error occurred while processing adopter statistic data.", e);
334         }
335       }
336     }
337   }
338 
339   public String getRegistrationDataAsString() throws Exception {
340     Adopter adopter = get();
341     if (null == adopter) {
342       adopter = new Adopter();
343     }
344     Map<String, Object> map = new LinkedHashMap<>();
345     map.put("general", new GeneralData(adopter));
346     map.put("statistics", collectStatisticData(adopter.getAdopterKey(), adopter.getStatisticKey()));
347     db.exec(em -> {
348       TypedQuery<AdopterRegistrationExtra> q =
349           em.createNamedQuery("AdopterRegistrationExtra.findAll", AdopterRegistrationExtra.class);
350       q.getResultList().forEach(extra -> {
351         map.put(extra.getType(),gson.fromJson(extra.getData(), Map.class));
352       });
353     });
354 
355     return gson.toJson(map);
356   }
357 
358 
359   //================================================================================
360   // Data collecting methods
361   //================================================================================
362 
363   /**
364    * Just retrieves the form data of the adopter.
365    * @param adopterRegistrationAdopter The adopter registration form.
366    * @return The adopter form containing general data as JSON string.
367    */
368   private String collectGeneralData(Adopter adopterRegistrationAdopter) {
369     GeneralData generalData = new GeneralData(adopterRegistrationAdopter);
370     return generalData.jsonify();
371   }
372 
373   /**
374    * Gathers various statistic data.
375    * @param statisticKey A Unique key per adopter for the statistic entry.
376    * @return The statistic data as JSON string.
377    * @throws Exception General exception that can occur while gathering data.
378    */
379   private StatisticData collectStatisticData(String adopterKey, String statisticKey) throws Exception {
380     StatisticData statisticData = new StatisticData(statisticKey);
381     statisticData.setAdopterKey(adopterKey);
382     serviceRegistry.getHostRegistrations().forEach(host -> {
383       Host h = new Host(host);
384       try {
385         String services = serviceRegistry.getServiceRegistrationsByHost(host.getBaseUrl())
386             .stream()
387             .map(sr -> sr.getServiceType())
388             .collect(Collectors.joining(",\n"));
389         h.setServices(services);
390       } catch (ServiceRegistryException e) {
391         logger.warn("Error gathering services for {}", host.getBaseUrl(), e);
392       }
393       statisticData.addHost(h);
394     });
395     statisticData.setJobCount(serviceRegistry.count(null, null));
396 
397     statisticData.setSeriesCount(seriesService.getSeriesCount());
398 
399     List<Organization> orgs = organizationDirectoryService.getOrganizations();
400     statisticData.setTenantCount(orgs.size());
401 
402     for (Organization org : orgs) {
403       SecurityUtil.runAs(securityService, org, systemAdminUser, () -> {
404         statisticData.setEventCount(statisticData.getEventCount() + assetManager.countEvents(org.getId()));
405 
406         //Calculate the number of attached CAs for this org, add it to the total
407         long current = statisticData.getCACount();
408         int orgCAs = caStateService.getKnownAgents().size();
409         statisticData.setCACount(current + orgCAs);
410 
411         final SearchSourceBuilder q = new SearchSourceBuilder().query(
412                 QueryBuilders.boolQuery()
413                     .must(QueryBuilders.termQuery(SearchResult.TYPE, SearchService.IndexEntryType.Episode))
414                     .must(QueryBuilders.termQuery(SearchResult.ORG, org.getId()))
415                     .mustNot(QueryBuilders.existsQuery(SearchResult.DELETED_DATE)));
416         final SearchResultList results = searchService.search(q);
417         long orgMilis = results.getHits().stream().map(
418                 result -> EncodingSchemeUtils.decodeDuration(Objects.toString(
419                     result.getDublinCore().getFirst(DublinCore.PROPERTY_EXTENT),
420                     "0")))
421             .filter(Objects::nonNull)
422             .reduce(Long::sum).orElse(0L);
423         statisticData.setTotalMinutes(statisticData.getTotalMinutes() + (orgMilis / 1000 / 60));
424 
425         //Add the users for each org
426         long currentUsers = statisticData.getUserCount();
427         statisticData.setUserCount(currentUsers + userProvider.countUsers() + userRefProvider.countUsers());
428       });
429     }
430     statisticData.setVersion(version);
431     return statisticData;
432   }
433 
434 
435   //================================================================================
436   // OSGi setter
437   //================================================================================
438 
439   /** OSGi setter for the service registry. */
440   @Reference
441   public void setServiceRegistry(ServiceRegistry serviceRegistry) {
442     this.serviceRegistry = serviceRegistry;
443   }
444 
445   @Reference
446   public void setCaptureAdminService(CaptureAgentStateService stateService) {
447     this.caStateService = stateService;
448   }
449 
450   /** OSGi setter for the asset manager. */
451   @Reference
452   public void setAssetManager(AssetManager assetManager) {
453     this.assetManager = assetManager;
454   }
455 
456   /** OSGi setter for the series service. */
457   @Reference
458   public void setSeriesService(SeriesService seriesService) {
459     this.seriesService = seriesService;
460   }
461 
462   @Reference
463   public void setSearchService(SearchService searchService) {
464     this.searchService = searchService;
465   }
466 
467   /** OSGi setter for the userref provider. */
468   @Reference
469   public void setUserRefProvider(JpaUserReferenceProvider userRefProvider) {
470     this.userRefProvider = userRefProvider;
471   }
472 
473   /* OSGi setter for the user provider. */
474   @Reference
475   public void setUserAndRoleProvider(JpaUserAndRoleProvider userProvider) {
476     this.userProvider = userProvider;
477   }
478 
479   /** OSGi callback for setting the security service. */
480   @Reference
481   public void setSecurityService(SecurityService securityService) {
482     this.securityService = securityService;
483   }
484 
485   /** OSGi callback for setting the org directory service. */
486   @Reference
487   public void setOrganizationDirectoryService(OrganizationDirectoryService orgDirServ) {
488     this.organizationDirectoryService = orgDirServ;
489   }
490 }