1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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
70
71
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
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
101 protected EntityManagerFactory emf = null;
102
103 protected DBSessionFactory dbSessionFactory;
104
105 protected DBSession db;
106
107
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
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
183 if (userActions.isEmpty()) {
184 action.setSession(userSession);
185 em.persist(action);
186 return action;
187 }
188
189
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
195 action.setId(lastAction.getId());
196 lastAction.setOutpoint(action.getOutpoint());
197 em.persist(lastAction);
198 return lastAction;
199 }
200
201
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
236
237 try {
238
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
247 EntityTransaction tx = em.getTransaction();
248 tx.commit();
249 tx.begin();
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
504 try {
505 calBegin.setTime(complex.parse(from));
506 } catch (ParseException e) {
507 calBegin.setTime(simple.parse(from));
508 }
509
510
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
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
585
586
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
601
602
603
604 @Override
605 public boolean getUserTrackingEnabled() {
606 return detailedTracking;
607 }
608 }