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) 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
177 if (userActions.isEmpty()) {
178 action.setSession(userSession);
179 em.persist(action);
180 return action;
181 }
182
183
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
189 action.setId(lastAction.getId());
190 lastAction.setOutpoint(action.getOutpoint());
191 em.persist(lastAction);
192 return lastAction;
193 }
194
195
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
224
225 try {
226
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
235 EntityTransaction tx = em.getTransaction();
236 tx.commit();
237 tx.begin();
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
491 try {
492 calBegin.setTime(complex.parse(from));
493 } catch (ParseException e) {
494 calBegin.setTime(simple.parse(from));
495 }
496
497
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
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
572
573
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
588
589
590
591 @Override
592 public boolean getUserTrackingEnabled() {
593 return detailedTracking;
594 }
595 }