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) session.setUserIp("-omitted-");
164     if (!logUser) session.setUserId("-omitted-");
165     if (!logSession) session.setSessionId("-omitted-");
166 
167     try {
168       return db.execTx(em -> {
169         UserSession userSession = populateSession(em, session);
170         List<UserAction> userActions = em
171             .createNamedQuery("findLastUserFootprintOfSession", UserAction.class)
172             .setParameter("session", userSession)
173             .setMaxResults(1)
174             .getResultList();
175 
176         // no actions
177         if (userActions.isEmpty()) {
178           action.setSession(userSession);
179           em.persist(action);
180           return action;
181         }
182 
183         // found last action
184         UserAction lastAction = userActions.iterator().next();
185         if (lastAction.getMediapackageId().equals(action.getMediapackageId())
186             && lastAction.getType().equals(action.getType())
187             && lastAction.getOutpoint() == action.getInpoint()) {
188           // we are assuming in this case that the sessions match and are unchanged (IP wise, for example)
189           action.setId(lastAction.getId());
190           lastAction.setOutpoint(action.getOutpoint());
191           em.persist(lastAction);
192           return lastAction;
193         }
194 
195         // last action does not match current action
196         action.setSession(userSession);
197         em.persist(action);
198         return action;
199       });
200     } catch (Exception e) {
201       throw new UserTrackingException(e);
202     }
203   }
204 
205   public UserAction addUserTrackingEvent(UserAction a, UserSession session) throws UserTrackingException {
206     if (!logIp) session.setUserIp("-omitted-");
207     if (!logUser) session.setUserId("-omitted-");
208     if (!logSession) session.setSessionId("-omitted-");
209 
210     try {
211       return db.execTx(em -> {
212         UserSession userSession = populateSession(em, session);
213         a.setSession(userSession);
214         em.persist(a);
215         return a;
216       });
217     } catch (Exception e) {
218       throw new UserTrackingException(e);
219     }
220   }
221 
222   private synchronized UserSession populateSession(EntityManager em, UserSession session) {
223     // assumption: this code is only called inside a DB transaction
224     //             => transaction retries are handled outside this method
225     try {
226       // Try and find the session. If not found, persist it
227       return namedQuery.find(
228           "findUserSessionBySessionId",
229           UserSession.class,
230           Pair.of("sessionId", session.getSessionId())
231       ).apply(em);
232     } catch (NoResultException n) {
233       em.persist(session);
234       // Commit the session object so that it's immediately found by other threads
235       EntityTransaction tx = em.getTransaction();
236       tx.commit();
237       tx.begin(); // start a new transaction to continue after session population
238     }
239     return session;
240   }
241 
242   public UserActionList getUserActions(int offset, int limit) {
243     UserActionList result = new UserActionListImpl();
244 
245     db.exec(em -> {
246       result.setTotal(getTotalQuery().apply(em));
247       result.setOffset(offset);
248       result.setLimit(limit);
249 
250       TypedQuery<UserAction> q = em
251           .createNamedQuery("findUserActions", UserAction.class)
252           .setFirstResult(offset);
253       if (limit > 0) {
254         q.setMaxResults(limit);
255       }
256       q.getResultList().forEach(result::add);
257     });
258 
259     return result;
260   }
261 
262   private Function<EntityManager, Integer> getTotalQuery() {
263     return namedQuery.find("findTotal", Long.class)
264         .andThen(Long::intValue);
265   }
266 
267   public UserActionList getUserActionsByType(String type, int offset, int limit) {
268     UserActionList result = new UserActionListImpl();
269 
270     db.exec(em -> {
271       result.setTotal(getTotalQuery(type).apply(em));
272       result.setOffset(offset);
273       result.setLimit(limit);
274 
275       TypedQuery<UserAction> q = em
276           .createNamedQuery("findUserActionsByType", UserAction.class)
277           .setParameter("type", type)
278           .setFirstResult(offset);
279       if (limit > 0) {
280         q.setMaxResults(limit);
281       }
282       q.getResultList().forEach(result::add);
283     });
284 
285     return result;
286   }
287 
288   private Function<EntityManager, Integer> getTotalQuery(String type) {
289     return namedQuery.find(
290         "findTotalByType",
291         Long.class,
292         Pair.of("type", type)
293     ).andThen(Long::intValue);
294   }
295 
296   public UserActionList getUserActionsByTypeAndMediapackageId(String type, String mediapackageId, int offset, int limit) {
297     UserActionList result = new UserActionListImpl();
298 
299     db.exec(em -> {
300       result.setTotal(getTotalQuery(type, mediapackageId).apply(em));
301       result.setOffset(offset);
302       result.setLimit(limit);
303 
304       TypedQuery<UserAction> q = em
305           .createNamedQuery("findUserActionsByTypeAndMediapackageId", UserAction.class)
306           .setParameter("type", type)
307           .setParameter("mediapackageId", mediapackageId)
308           .setFirstResult(offset);
309       if (limit > 0) {
310         q.setMaxResults(limit);
311       }
312       q.getResultList().forEach(result::add);
313     });
314 
315     return result;
316   }
317 
318   public UserActionList getUserActionsByTypeAndDay(String type, String day, int offset, int limit) {
319     UserActionList result = new UserActionListImpl();
320 
321     int year = Integer.parseInt(day.substring(0, 4));
322     int month = Integer.parseInt(day.substring(4, 6)) - 1;
323     int date = Integer.parseInt(day.substring(6, 8));
324 
325     Calendar calBegin = new GregorianCalendar();
326     calBegin.set(year, month, date, 0, 0);
327     Calendar calEnd = new GregorianCalendar();
328     calEnd.set(year, month, date, 23, 59);
329 
330     db.exec(em -> {
331       result.setTotal(getTotalQuery(type, calBegin, calEnd).apply(em));
332       result.setOffset(offset);
333       result.setLimit(limit);
334 
335       TypedQuery<UserAction> q = em
336           .createNamedQuery("findUserActionsByTypeAndIntervall", UserAction.class)
337           .setParameter("type", type)
338           .setParameter("begin", calBegin, TemporalType.TIMESTAMP)
339           .setParameter("end", calEnd, TemporalType.TIMESTAMP)
340           .setFirstResult(offset);
341       if (limit > 0) {
342         q.setMaxResults(limit);
343       }
344       q.getResultList().forEach(result::add);
345     });
346 
347     return result;
348   }
349 
350   public UserActionList getUserActionsByTypeAndMediapackageIdByDate(String type, String mediapackageId, int offset,
351           int limit) {
352     UserActionList result = new UserActionListImpl();
353 
354     db.exec(em -> {
355       result.setTotal(getTotalQuery(type, mediapackageId).apply(em));
356       result.setOffset(offset);
357       result.setLimit(limit);
358 
359       TypedQuery<UserAction> q = em
360           .createNamedQuery("findUserActionsByMediaPackageAndTypeAscendingByDate", UserAction.class)
361           .setParameter("type", type)
362           .setParameter("mediapackageId", mediapackageId)
363           .setFirstResult(offset);
364       if (limit > 0) {
365         q.setMaxResults(limit);
366       }
367       q.getResultList().forEach(result::add);
368     });
369 
370     return result;
371   }
372 
373   public UserActionList getUserActionsByTypeAndMediapackageIdByDescendingDate(String type, String mediapackageId,
374           int offset, int limit) {
375     UserActionList result = new UserActionListImpl();
376 
377     db.exec(em -> {
378       result.setTotal(getTotalQuery(type, mediapackageId).apply(em));
379       result.setOffset(offset);
380       result.setLimit(limit);
381 
382       TypedQuery<UserAction> q = em
383           .createNamedQuery("findUserActionsByMediaPackageAndTypeDescendingByDate", UserAction.class)
384           .setParameter("type", type)
385           .setParameter("mediapackageId", mediapackageId)
386           .setFirstResult(offset);
387       if (limit > 0) {
388         q.setMaxResults(limit);
389       }
390       q.getResultList().forEach(result::add);
391     });
392 
393     return result;
394   }
395 
396   private Function<EntityManager, Integer> getTotalQuery(String type, Calendar calBegin, Calendar calEnd) {
397     return namedQuery.find(
398         "findTotalByTypeAndIntervall",
399         Long.class,
400         Pair.of("type", type),
401         Pair.of("begin", calBegin),
402         Pair.of("end", calEnd)
403     ).andThen(Long::intValue);
404   }
405 
406   private Function<EntityManager, Integer> getTotalQuery(String type, String mediapackageId) {
407     return namedQuery.find(
408         "findTotalByTypeAndMediapackageId",
409         Long.class,
410         Pair.of("type", type),
411         Pair.of("mediapackageId", mediapackageId)
412     ).andThen(Long::intValue);
413   }
414 
415   public UserActionList getUserActionsByDay(String day, int offset, int limit) {
416     UserActionList result = new UserActionListImpl();
417 
418     int year = Integer.parseInt(day.substring(0, 4));
419     int month = Integer.parseInt(day.substring(4, 6)) - 1;
420     int date = Integer.parseInt(day.substring(6, 8));
421 
422     Calendar calBegin = new GregorianCalendar();
423     calBegin.set(year, month, date, 0, 0);
424     Calendar calEnd = new GregorianCalendar();
425     calEnd.set(year, month, date, 23, 59);
426 
427     db.exec(em -> {
428       result.setTotal(getTotalQuery(calBegin, calEnd).apply(em));
429       result.setOffset(offset);
430       result.setLimit(limit);
431 
432       TypedQuery<UserAction> q = em
433           .createNamedQuery("findUserActionsByIntervall", UserAction.class)
434           .setParameter("begin", calBegin, TemporalType.TIMESTAMP)
435           .setParameter("end", calEnd, TemporalType.TIMESTAMP)
436           .setFirstResult(offset);
437       if (limit > 0) {
438         q.setMaxResults(limit);
439       }
440       q.getResultList().forEach(result::add);
441     });
442 
443     return result;
444   }
445 
446   private Function<EntityManager, Integer> getTotalQuery(Calendar calBegin, Calendar calEnd) {
447     return namedQuery.find(
448         "findTotalByIntervall",
449         Long.class,
450         Pair.of("begin", calBegin),
451         Pair.of("end", calEnd)
452     ).andThen(Long::intValue);
453   }
454 
455   public Report getReport(int offset, int limit) {
456     Report report = new ReportImpl();
457     report.setLimit(limit);
458     report.setOffset(offset);
459 
460     db.exec(em -> {
461       TypedQuery<Object[]> q = em
462           .createNamedQuery("countSessionsGroupByMediapackage", Object[].class)
463           .setFirstResult(offset);
464       if (limit > 0) {
465         q.setMaxResults(limit);
466       }
467 
468       q.getResultList().forEach(row -> {
469         ReportItem item = new ReportItemImpl();
470         item.setEpisodeId((String) row[0]);
471         item.setViews((Long) row[1]);
472         item.setPlayed((Long) row[2]);
473         report.add(item);
474       });
475     });
476 
477     return report;
478   }
479 
480   public Report getReport(String from, String to, int offset, int limit) throws ParseException {
481     Report report = new ReportImpl();
482     report.setLimit(limit);
483     report.setOffset(offset);
484 
485     Calendar calBegin = new GregorianCalendar();
486     Calendar calEnd = new GregorianCalendar();
487     SimpleDateFormat complex = new SimpleDateFormat("yyyyMMddhhmm");
488     SimpleDateFormat simple = new SimpleDateFormat("yyyyMMdd");
489 
490     // Try to parse the from calendar
491     try {
492       calBegin.setTime(complex.parse(from));
493     } catch (ParseException e) {
494       calBegin.setTime(simple.parse(from));
495     }
496 
497     // Try to parse the to calendar
498     try {
499       calEnd.setTime(complex.parse(to));
500     } catch (ParseException e) {
501       calEnd.setTime(simple.parse(to));
502     }
503 
504     db.exec(em -> {
505       TypedQuery<Object[]> q = em
506           .createNamedQuery("countSessionsGroupByMediapackageByIntervall", Object[].class)
507           .setParameter("begin", calBegin, TemporalType.TIMESTAMP)
508           .setParameter("end", calEnd, TemporalType.TIMESTAMP)
509           .setFirstResult(offset);
510       if (limit > 0) {
511         q.setMaxResults(limit);
512       }
513 
514       q.getResultList().forEach(row -> {
515         ReportItem item = new ReportItemImpl();
516         item.setEpisodeId((String) row[0]);
517         item.setViews((Long) row[1]);
518         item.setPlayed((Long) row[2]);
519         report.add(item);
520       });
521     });
522 
523     return report;
524   }
525 
526   public FootprintList getFootprints(String mediapackageId, String userId) {
527     List<UserAction> userActions = db.exec(em -> {
528       TypedQuery<UserAction> q;
529       if (!logUser || StringUtils.trimToNull(userId) == null) {
530         q = em.createNamedQuery("findUserActionsByTypeAndMediapackageIdOrderByOutpointDESC", UserAction.class);
531       } else {
532         q = em.createNamedQuery("findUserActionsByTypeAndMediapackageIdByUserOrderByOutpointDESC",
533                 UserAction.class)
534             .setParameter("userid", userId);
535       }
536       q.setParameter("type", FOOTPRINT_KEY);
537       q.setParameter("mediapackageId", mediapackageId);
538       return q.getResultList();
539     });
540 
541     int[] resultArray = new int[1];
542     boolean first = true;
543 
544     for (UserAction a : userActions) {
545       if (first) {
546         // Get one more item than the known outpoint to append a footprint of 0 views at the end of the result set
547         resultArray = new int[a.getOutpoint() + 1];
548         first = false;
549       }
550       for (int i = a.getInpoint(); i < a.getOutpoint(); i++) {
551         resultArray[i]++;
552       }
553     }
554     FootprintList list = new FootprintsListImpl();
555     int current = -1;
556     int last = -1;
557     for (int i = 0; i < resultArray.length; i++) {
558       current = resultArray[i];
559       if (last != current) {
560         Footprint footprint = new FootprintImpl();
561         footprint.setPosition(i);
562         footprint.setViews(current);
563         list.add(footprint);
564       }
565       last = current;
566     }
567     return list;
568   }
569 
570   /**
571    * {@inheritDoc}
572    *
573    * @see org.opencastproject.usertracking.api.UserTrackingService#getUserAction(java.lang.Long)
574    */
575   @Override
576   public UserAction getUserAction(Long id) throws UserTrackingException, NotFoundException {
577     try {
578       return db.exec(namedQuery.findByIdOpt(UserActionImpl.class, id)).orElseThrow(NoResultException::new);
579     } catch (NoResultException e) {
580       throw new NotFoundException("No UserAction found with id='" + id + "'");
581     } catch (Exception e) {
582       throw new UserTrackingException(e);
583     }
584   }
585 
586   /**
587    * {@inheritDoc}
588    *
589    * @see org.opencastproject.usertracking.api.UserTrackingService#getUserTrackingEnabled()
590    */
591   @Override
592   public boolean getUserTrackingEnabled() {
593     return detailedTracking;
594   }
595 }