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.usertracking.impl;
23  
24  import static org.opencastproject.db.Queries.namedQuery;
25  
26  import org.opencastproject.db.DBSession;
27  import org.opencastproject.db.DBSessionFactory;
28  import org.opencastproject.usertracking.api.Footprint;
29  import org.opencastproject.usertracking.api.FootprintList;
30  import org.opencastproject.usertracking.api.Report;
31  import org.opencastproject.usertracking.api.ReportItem;
32  import org.opencastproject.usertracking.api.UserAction;
33  import org.opencastproject.usertracking.api.UserActionList;
34  import org.opencastproject.usertracking.api.UserSession;
35  import org.opencastproject.usertracking.api.UserTrackingException;
36  import org.opencastproject.usertracking.api.UserTrackingService;
37  import org.opencastproject.usertracking.endpoint.FootprintImpl;
38  import org.opencastproject.usertracking.endpoint.FootprintsListImpl;
39  import org.opencastproject.usertracking.endpoint.ReportImpl;
40  import org.opencastproject.usertracking.endpoint.ReportItemImpl;
41  import org.opencastproject.util.NotFoundException;
42  
43  import org.apache.commons.lang3.StringUtils;
44  import org.apache.commons.lang3.tuple.Pair;
45  import org.osgi.service.cm.ConfigurationException;
46  import org.osgi.service.cm.ManagedService;
47  import org.osgi.service.component.annotations.Activate;
48  import org.osgi.service.component.annotations.Component;
49  import org.osgi.service.component.annotations.Reference;
50  import org.slf4j.Logger;
51  import org.slf4j.LoggerFactory;
52  
53  import java.text.ParseException;
54  import java.text.SimpleDateFormat;
55  import java.util.Calendar;
56  import java.util.Dictionary;
57  import java.util.GregorianCalendar;
58  import java.util.List;
59  import java.util.function.Function;
60  
61  import javax.persistence.EntityManager;
62  import javax.persistence.EntityManagerFactory;
63  import javax.persistence.EntityTransaction;
64  import javax.persistence.NoResultException;
65  import javax.persistence.TemporalType;
66  import javax.persistence.TypedQuery;
67  
68  /**
69   * Implementation of org.opencastproject.usertracking.api.UserTrackingService
70   *
71   * @see org.opencastproject.usertracking.api.UserTrackingService
72   */
73  @Component(
74      immediate = true,
75      service = { UserTrackingService.class,ManagedService.class },
76      property = {
77          "service.description=User Tracking Service",
78          "service.pid=org.opencastproject.usertracking.impl.UserTrackingServiceImpl"
79      }
80  )
81  public class UserTrackingServiceImpl implements UserTrackingService, ManagedService {
82  
83    /** JPA persistence unit name */
84    public static final String PERSISTENCE_UNIT = "org.opencastproject.usertracking";
85  
86    public static final String FOOTPRINT_KEY = "FOOTPRINT";
87  
88    public static final String DETAILED_TRACKING = "org.opencastproject.usertracking.detailedtrack";
89    public static final String IP_LOGGING = "org.opencastproject.usertracking.log.ip";
90    public static final String USER_LOGGING = "org.opencastproject.usertracking.log.user";
91    public static final String SESSION_LOGGING = "org.opencastproject.usertracking.log.session";
92  
93    private static final Logger logger = LoggerFactory.getLogger(UserTrackingServiceImpl.class);
94  
95    private boolean detailedTracking = false;
96    private boolean logIp = true;
97    private boolean logUser = true;
98    private boolean logSession = true;
99  
100   /** The factory used to generate the entity manager */
101   protected EntityManagerFactory emf = null;
102 
103   protected DBSessionFactory dbSessionFactory;
104 
105   protected DBSession db;
106 
107   /** OSGi DI */
108   @Reference(target = "(osgi.unit.name=org.opencastproject.usertracking)")
109   void setEntityManagerFactory(EntityManagerFactory emf) {
110     this.emf = emf;
111   }
112 
113   @Reference
114   public void setDBSessionFactory(DBSessionFactory dbSessionFactory) {
115     this.dbSessionFactory = dbSessionFactory;
116   }
117 
118   /**
119    * Activation callback to be executed once all dependencies are set
120    */
121   @Activate
122   public void activate() {
123     logger.debug("activate()");
124     db = dbSessionFactory.createSession(emf);
125   }
126 
127   @Override
128   public void updated(Dictionary props) throws ConfigurationException {
129     if (props == null) {
130       logger.debug("Null properties in user tracking service, not doing detailed logging");
131       return;
132     }
133 
134     Object val = props.get(DETAILED_TRACKING);
135     if (val != null && String.class.isInstance(val)) {
136       detailedTracking = Boolean.valueOf((String) val);
137     }
138     val = props.get(IP_LOGGING);
139     if (val != null && String.class.isInstance(val)) {
140       logIp = Boolean.valueOf((String) val);
141     }
142     val = props.get(USER_LOGGING);
143     if (val != null && String.class.isInstance(val)) {
144       logUser = Boolean.valueOf((String) val);
145     }
146     val = props.get(SESSION_LOGGING);
147     if (val != null && String.class.isInstance(val)) {
148       logSession = Boolean.valueOf((String) val);
149     }
150 
151   }
152 
153   public int getViews(String mediapackageId) {
154     return db.exec(namedQuery.find(
155         "countSessionsOfMediapackage",
156         Long.class,
157         Pair.of("mediapackageId", mediapackageId)
158     )).intValue();
159   }
160 
161   public UserAction addUserFootprint(UserAction action, UserSession session) throws UserTrackingException {
162     action.setType(FOOTPRINT_KEY);
163     if (!logIp) {
164       session.setUserIp("-omitted-");
165     }
166     if (!logUser) {
167       session.setUserId("-omitted-");
168     }
169     if (!logSession) {
170       session.setSessionId("-omitted-");
171     }
172 
173     try {
174       return db.execTx(em -> {
175         UserSession userSession = populateSession(em, session);
176         List<UserAction> userActions = em
177             .createNamedQuery("findLastUserFootprintOfSession", UserAction.class)
178             .setParameter("session", userSession)
179             .setMaxResults(1)
180             .getResultList();
181 
182         // no actions
183         if (userActions.isEmpty()) {
184           action.setSession(userSession);
185           em.persist(action);
186           return action;
187         }
188 
189         // found last action
190         UserAction lastAction = userActions.iterator().next();
191         if (lastAction.getMediapackageId().equals(action.getMediapackageId())
192             && lastAction.getType().equals(action.getType())
193             && lastAction.getOutpoint() == action.getInpoint()) {
194           // we are assuming in this case that the sessions match and are unchanged (IP wise, for example)
195           action.setId(lastAction.getId());
196           lastAction.setOutpoint(action.getOutpoint());
197           em.persist(lastAction);
198           return lastAction;
199         }
200 
201         // last action does not match current action
202         action.setSession(userSession);
203         em.persist(action);
204         return action;
205       });
206     } catch (Exception e) {
207       throw new UserTrackingException(e);
208     }
209   }
210 
211   public UserAction addUserTrackingEvent(UserAction a, UserSession session) throws UserTrackingException {
212     if (!logIp) {
213       session.setUserIp("-omitted-");
214     }
215     if (!logUser) {
216       session.setUserId("-omitted-");
217     }
218     if (!logSession) {
219       session.setSessionId("-omitted-");
220     }
221 
222     try {
223       return db.execTx(em -> {
224         UserSession userSession = populateSession(em, session);
225         a.setSession(userSession);
226         em.persist(a);
227         return a;
228       });
229     } catch (Exception e) {
230       throw new UserTrackingException(e);
231     }
232   }
233 
234   private synchronized UserSession populateSession(EntityManager em, UserSession session) {
235     // assumption: this code is only called inside a DB transaction
236     //             => transaction retries are handled outside this method
237     try {
238       // Try and find the session. If not found, persist it
239       return namedQuery.find(
240           "findUserSessionBySessionId",
241           UserSession.class,
242           Pair.of("sessionId", session.getSessionId())
243       ).apply(em);
244     } catch (NoResultException n) {
245       em.persist(session);
246       // Commit the session object so that it's immediately found by other threads
247       EntityTransaction tx = em.getTransaction();
248       tx.commit();
249       tx.begin(); // start a new transaction to continue after session population
250     }
251     return session;
252   }
253 
254   public UserActionList getUserActions(int offset, int limit) {
255     UserActionList result = new UserActionListImpl();
256 
257     db.exec(em -> {
258       result.setTotal(getTotalQuery().apply(em));
259       result.setOffset(offset);
260       result.setLimit(limit);
261 
262       TypedQuery<UserAction> q = em
263           .createNamedQuery("findUserActions", UserAction.class)
264           .setFirstResult(offset);
265       if (limit > 0) {
266         q.setMaxResults(limit);
267       }
268       q.getResultList().forEach(result::add);
269     });
270 
271     return result;
272   }
273 
274   private Function<EntityManager, Integer> getTotalQuery() {
275     return namedQuery.find("findTotal", Long.class)
276         .andThen(Long::intValue);
277   }
278 
279   public UserActionList getUserActionsByType(String type, int offset, int limit) {
280     UserActionList result = new UserActionListImpl();
281 
282     db.exec(em -> {
283       result.setTotal(getTotalQuery(type).apply(em));
284       result.setOffset(offset);
285       result.setLimit(limit);
286 
287       TypedQuery<UserAction> q = em
288           .createNamedQuery("findUserActionsByType", UserAction.class)
289           .setParameter("type", type)
290           .setFirstResult(offset);
291       if (limit > 0) {
292         q.setMaxResults(limit);
293       }
294       q.getResultList().forEach(result::add);
295     });
296 
297     return result;
298   }
299 
300   private Function<EntityManager, Integer> getTotalQuery(String type) {
301     return namedQuery.find(
302         "findTotalByType",
303         Long.class,
304         Pair.of("type", type)
305     ).andThen(Long::intValue);
306   }
307 
308   public UserActionList getUserActionsByTypeAndMediapackageId(String type, String mediapackageId,
309       int offset, int limit) {
310     UserActionList result = new UserActionListImpl();
311 
312     db.exec(em -> {
313       result.setTotal(getTotalQuery(type, mediapackageId).apply(em));
314       result.setOffset(offset);
315       result.setLimit(limit);
316 
317       TypedQuery<UserAction> q = em
318           .createNamedQuery("findUserActionsByTypeAndMediapackageId", UserAction.class)
319           .setParameter("type", type)
320           .setParameter("mediapackageId", mediapackageId)
321           .setFirstResult(offset);
322       if (limit > 0) {
323         q.setMaxResults(limit);
324       }
325       q.getResultList().forEach(result::add);
326     });
327 
328     return result;
329   }
330 
331   public UserActionList getUserActionsByTypeAndDay(String type, String day, int offset, int limit) {
332     UserActionList result = new UserActionListImpl();
333 
334     int year = Integer.parseInt(day.substring(0, 4));
335     int month = Integer.parseInt(day.substring(4, 6)) - 1;
336     int date = Integer.parseInt(day.substring(6, 8));
337 
338     Calendar calBegin = new GregorianCalendar();
339     calBegin.set(year, month, date, 0, 0);
340     Calendar calEnd = new GregorianCalendar();
341     calEnd.set(year, month, date, 23, 59);
342 
343     db.exec(em -> {
344       result.setTotal(getTotalQuery(type, calBegin, calEnd).apply(em));
345       result.setOffset(offset);
346       result.setLimit(limit);
347 
348       TypedQuery<UserAction> q = em
349           .createNamedQuery("findUserActionsByTypeAndIntervall", UserAction.class)
350           .setParameter("type", type)
351           .setParameter("begin", calBegin, TemporalType.TIMESTAMP)
352           .setParameter("end", calEnd, TemporalType.TIMESTAMP)
353           .setFirstResult(offset);
354       if (limit > 0) {
355         q.setMaxResults(limit);
356       }
357       q.getResultList().forEach(result::add);
358     });
359 
360     return result;
361   }
362 
363   public UserActionList getUserActionsByTypeAndMediapackageIdByDate(String type, String mediapackageId, int offset,
364           int limit) {
365     UserActionList result = new UserActionListImpl();
366 
367     db.exec(em -> {
368       result.setTotal(getTotalQuery(type, mediapackageId).apply(em));
369       result.setOffset(offset);
370       result.setLimit(limit);
371 
372       TypedQuery<UserAction> q = em
373           .createNamedQuery("findUserActionsByMediaPackageAndTypeAscendingByDate", UserAction.class)
374           .setParameter("type", type)
375           .setParameter("mediapackageId", mediapackageId)
376           .setFirstResult(offset);
377       if (limit > 0) {
378         q.setMaxResults(limit);
379       }
380       q.getResultList().forEach(result::add);
381     });
382 
383     return result;
384   }
385 
386   public UserActionList getUserActionsByTypeAndMediapackageIdByDescendingDate(String type, String mediapackageId,
387           int offset, int limit) {
388     UserActionList result = new UserActionListImpl();
389 
390     db.exec(em -> {
391       result.setTotal(getTotalQuery(type, mediapackageId).apply(em));
392       result.setOffset(offset);
393       result.setLimit(limit);
394 
395       TypedQuery<UserAction> q = em
396           .createNamedQuery("findUserActionsByMediaPackageAndTypeDescendingByDate", UserAction.class)
397           .setParameter("type", type)
398           .setParameter("mediapackageId", mediapackageId)
399           .setFirstResult(offset);
400       if (limit > 0) {
401         q.setMaxResults(limit);
402       }
403       q.getResultList().forEach(result::add);
404     });
405 
406     return result;
407   }
408 
409   private Function<EntityManager, Integer> getTotalQuery(String type, Calendar calBegin, Calendar calEnd) {
410     return namedQuery.find(
411         "findTotalByTypeAndIntervall",
412         Long.class,
413         Pair.of("type", type),
414         Pair.of("begin", calBegin),
415         Pair.of("end", calEnd)
416     ).andThen(Long::intValue);
417   }
418 
419   private Function<EntityManager, Integer> getTotalQuery(String type, String mediapackageId) {
420     return namedQuery.find(
421         "findTotalByTypeAndMediapackageId",
422         Long.class,
423         Pair.of("type", type),
424         Pair.of("mediapackageId", mediapackageId)
425     ).andThen(Long::intValue);
426   }
427 
428   public UserActionList getUserActionsByDay(String day, int offset, int limit) {
429     UserActionList result = new UserActionListImpl();
430 
431     int year = Integer.parseInt(day.substring(0, 4));
432     int month = Integer.parseInt(day.substring(4, 6)) - 1;
433     int date = Integer.parseInt(day.substring(6, 8));
434 
435     Calendar calBegin = new GregorianCalendar();
436     calBegin.set(year, month, date, 0, 0);
437     Calendar calEnd = new GregorianCalendar();
438     calEnd.set(year, month, date, 23, 59);
439 
440     db.exec(em -> {
441       result.setTotal(getTotalQuery(calBegin, calEnd).apply(em));
442       result.setOffset(offset);
443       result.setLimit(limit);
444 
445       TypedQuery<UserAction> q = em
446           .createNamedQuery("findUserActionsByIntervall", UserAction.class)
447           .setParameter("begin", calBegin, TemporalType.TIMESTAMP)
448           .setParameter("end", calEnd, TemporalType.TIMESTAMP)
449           .setFirstResult(offset);
450       if (limit > 0) {
451         q.setMaxResults(limit);
452       }
453       q.getResultList().forEach(result::add);
454     });
455 
456     return result;
457   }
458 
459   private Function<EntityManager, Integer> getTotalQuery(Calendar calBegin, Calendar calEnd) {
460     return namedQuery.find(
461         "findTotalByIntervall",
462         Long.class,
463         Pair.of("begin", calBegin),
464         Pair.of("end", calEnd)
465     ).andThen(Long::intValue);
466   }
467 
468   public Report getReport(int offset, int limit) {
469     Report report = new ReportImpl();
470     report.setLimit(limit);
471     report.setOffset(offset);
472 
473     db.exec(em -> {
474       TypedQuery<Object[]> q = em
475           .createNamedQuery("countSessionsGroupByMediapackage", Object[].class)
476           .setFirstResult(offset);
477       if (limit > 0) {
478         q.setMaxResults(limit);
479       }
480 
481       q.getResultList().forEach(row -> {
482         ReportItem item = new ReportItemImpl();
483         item.setEpisodeId((String) row[0]);
484         item.setViews((Long) row[1]);
485         item.setPlayed((Long) row[2]);
486         report.add(item);
487       });
488     });
489 
490     return report;
491   }
492 
493   public Report getReport(String from, String to, int offset, int limit) throws ParseException {
494     Report report = new ReportImpl();
495     report.setLimit(limit);
496     report.setOffset(offset);
497 
498     Calendar calBegin = new GregorianCalendar();
499     Calendar calEnd = new GregorianCalendar();
500     SimpleDateFormat complex = new SimpleDateFormat("yyyyMMddhhmm");
501     SimpleDateFormat simple = new SimpleDateFormat("yyyyMMdd");
502 
503     // Try to parse the from calendar
504     try {
505       calBegin.setTime(complex.parse(from));
506     } catch (ParseException e) {
507       calBegin.setTime(simple.parse(from));
508     }
509 
510     // Try to parse the to calendar
511     try {
512       calEnd.setTime(complex.parse(to));
513     } catch (ParseException e) {
514       calEnd.setTime(simple.parse(to));
515     }
516 
517     db.exec(em -> {
518       TypedQuery<Object[]> q = em
519           .createNamedQuery("countSessionsGroupByMediapackageByIntervall", Object[].class)
520           .setParameter("begin", calBegin, TemporalType.TIMESTAMP)
521           .setParameter("end", calEnd, TemporalType.TIMESTAMP)
522           .setFirstResult(offset);
523       if (limit > 0) {
524         q.setMaxResults(limit);
525       }
526 
527       q.getResultList().forEach(row -> {
528         ReportItem item = new ReportItemImpl();
529         item.setEpisodeId((String) row[0]);
530         item.setViews((Long) row[1]);
531         item.setPlayed((Long) row[2]);
532         report.add(item);
533       });
534     });
535 
536     return report;
537   }
538 
539   public FootprintList getFootprints(String mediapackageId, String userId) {
540     List<UserAction> userActions = db.exec(em -> {
541       TypedQuery<UserAction> q;
542       if (!logUser || StringUtils.trimToNull(userId) == null) {
543         q = em.createNamedQuery("findUserActionsByTypeAndMediapackageIdOrderByOutpointDESC", UserAction.class);
544       } else {
545         q = em.createNamedQuery("findUserActionsByTypeAndMediapackageIdByUserOrderByOutpointDESC",
546                 UserAction.class)
547             .setParameter("userid", userId);
548       }
549       q.setParameter("type", FOOTPRINT_KEY);
550       q.setParameter("mediapackageId", mediapackageId);
551       return q.getResultList();
552     });
553 
554     int[] resultArray = new int[1];
555     boolean first = true;
556 
557     for (UserAction a : userActions) {
558       if (first) {
559         // Get one more item than the known outpoint to append a footprint of 0 views at the end of the result set
560         resultArray = new int[a.getOutpoint() + 1];
561         first = false;
562       }
563       for (int i = a.getInpoint(); i < a.getOutpoint(); i++) {
564         resultArray[i]++;
565       }
566     }
567     FootprintList list = new FootprintsListImpl();
568     int current = -1;
569     int last = -1;
570     for (int i = 0; i < resultArray.length; i++) {
571       current = resultArray[i];
572       if (last != current) {
573         Footprint footprint = new FootprintImpl();
574         footprint.setPosition(i);
575         footprint.setViews(current);
576         list.add(footprint);
577       }
578       last = current;
579     }
580     return list;
581   }
582 
583   /**
584    * {@inheritDoc}
585    *
586    * @see org.opencastproject.usertracking.api.UserTrackingService#getUserAction(java.lang.Long)
587    */
588   @Override
589   public UserAction getUserAction(Long id) throws UserTrackingException, NotFoundException {
590     try {
591       return db.exec(namedQuery.findByIdOpt(UserActionImpl.class, id)).orElseThrow(NoResultException::new);
592     } catch (NoResultException e) {
593       throw new NotFoundException("No UserAction found with id='" + id + "'");
594     } catch (Exception e) {
595       throw new UserTrackingException(e);
596     }
597   }
598 
599   /**
600    * {@inheritDoc}
601    *
602    * @see org.opencastproject.usertracking.api.UserTrackingService#getUserTrackingEnabled()
603    */
604   @Override
605   public boolean getUserTrackingEnabled() {
606     return detailedTracking;
607   }
608 }