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.endpoint;
23
24 import static javax.servlet.http.HttpServletResponse.SC_CREATED;
25 import static javax.servlet.http.HttpServletResponse.SC_OK;
26
27 import org.opencastproject.rest.RestConstants;
28 import org.opencastproject.security.api.SecurityService;
29 import org.opencastproject.systems.OpencastConstants;
30 import org.opencastproject.usertracking.api.UserSession;
31 import org.opencastproject.usertracking.api.UserTrackingException;
32 import org.opencastproject.usertracking.api.UserTrackingService;
33 import org.opencastproject.usertracking.impl.UserActionImpl;
34 import org.opencastproject.usertracking.impl.UserActionListImpl;
35 import org.opencastproject.usertracking.impl.UserSessionImpl;
36 import org.opencastproject.util.NotFoundException;
37 import org.opencastproject.util.UrlSupport;
38 import org.opencastproject.util.doc.rest.RestParameter;
39 import org.opencastproject.util.doc.rest.RestParameter.Type;
40 import org.opencastproject.util.doc.rest.RestQuery;
41 import org.opencastproject.util.doc.rest.RestResponse;
42 import org.opencastproject.util.doc.rest.RestService;
43
44 import org.apache.commons.lang3.StringUtils;
45 import org.osgi.service.component.ComponentContext;
46 import org.osgi.service.component.annotations.Activate;
47 import org.osgi.service.component.annotations.Component;
48 import org.osgi.service.component.annotations.Reference;
49 import org.osgi.service.jaxrs.whiteboard.propertytypes.JaxrsResource;
50 import org.slf4j.Logger;
51 import org.slf4j.LoggerFactory;
52
53 import java.net.URI;
54 import java.net.URISyntaxException;
55 import java.text.ParseException;
56
57 import javax.servlet.http.HttpServletRequest;
58 import javax.ws.rs.FormParam;
59 import javax.ws.rs.GET;
60 import javax.ws.rs.PUT;
61 import javax.ws.rs.Path;
62 import javax.ws.rs.PathParam;
63 import javax.ws.rs.Produces;
64 import javax.ws.rs.QueryParam;
65 import javax.ws.rs.WebApplicationException;
66 import javax.ws.rs.core.Context;
67 import javax.ws.rs.core.MediaType;
68 import javax.ws.rs.core.Response;
69 import javax.ws.rs.core.Response.Status;
70
71
72
73
74 @Path("/usertracking")
75 @RestService(
76 name = "usertracking",
77 title = "User Tracking Service",
78 abstractText = "This service is used for tracking user interaction creates, edits and retrieves user actions and "
79 + "viewing statistics.",
80 notes = {
81 "All paths above are relative to the REST endpoint base (something like http://your.server/files)",
82 "If the service is down or not working it will return a status 503, this means the the underlying service is "
83 + "not working and is either restarting or has failed",
84 "A status code 500 means a general failure has occurred which is not recoverable and was not anticipated. In "
85 + "other words, there is a bug! You should file an error report with your server logs from the time "
86 + "when the error occurred: "
87 + "<a href=\"https://github.com/opencast/opencast/issues\">Opencast Issue Tracker</a>" })
88 @Component(
89 immediate = true,
90 service = UserTrackingRestService.class,
91 property = {
92 "service.description=User Tracking REST Endpoint",
93 "opencast.service.type=org.opencastproject.usertracking",
94 "opencast.service.path=/usertracking"
95 }
96 )
97 @JaxrsResource
98 public class UserTrackingRestService {
99
100 private static final Logger logger = LoggerFactory.getLogger(UserTrackingRestService.class);
101
102 private UserTrackingService usertrackingService;
103
104 protected SecurityService securityService;
105
106 protected String serverUrl = UrlSupport.DEFAULT_BASE_URL;
107
108 protected String serviceUrl = "/usertracking";
109
110
111
112
113
114
115 @Reference
116 public void setService(UserTrackingService service) {
117 this.usertrackingService = service;
118 }
119
120
121
122
123
124
125
126 @Reference
127 public void setSecurityService(SecurityService securityService) {
128 this.securityService = securityService;
129 }
130
131
132
133
134
135
136
137 @Activate
138 public void activate(ComponentContext cc) {
139
140 if (cc == null) {
141 serverUrl = UrlSupport.DEFAULT_BASE_URL;
142 } else {
143 String ccServerUrl = cc.getBundleContext().getProperty(OpencastConstants.SERVER_URL_PROPERTY);
144 logger.info("configured server url is {}", ccServerUrl);
145 if (ccServerUrl == null) {
146 serverUrl = UrlSupport.DEFAULT_BASE_URL;
147 } else {
148 serverUrl = ccServerUrl;
149 }
150 serviceUrl = (String) cc.getProperties().get(RestConstants.SERVICE_PATH_PROPERTY);
151 }
152 }
153
154
155
156
157 @GET
158 @Produces(MediaType.TEXT_XML)
159 @Path("/actions.xml")
160 @RestQuery(
161 name = "actionsasxml",
162 description = "Get user actions by type and day",
163 returnDescription = "The user actions.",
164 restParameters = {
165 @RestParameter(name = "type", description = "The type of the user action", isRequired = false,
166 type = Type.STRING),
167 @RestParameter(name = "day", description = "The day of creation (format: YYYYMMDD)", isRequired = false,
168 type = Type.STRING),
169 @RestParameter(name = "limit", description = "The maximum number of items to return per page",
170 isRequired = false, type = Type.INTEGER),
171 @RestParameter(name = "offset", description = "The page number", isRequired = false, type = Type.INTEGER)
172 },
173 responses = {
174 @RestResponse(responseCode = SC_OK, description = "An XML representation of the user actions")
175 })
176 public UserActionListImpl getUserActionsAsXml(@QueryParam("id") String id, @QueryParam("type") String type,
177 @QueryParam("day") String day, @QueryParam("limit") int limit, @QueryParam("offset") int offset) {
178
179
180 if (offset < 0 || limit < 0) {
181 throw new WebApplicationException(Status.BAD_REQUEST);
182 }
183
184
185 if (limit == 0) {
186 limit = 10;
187 }
188 try {
189 if (!StringUtils.isEmpty(id) && !StringUtils.isEmpty(type)) {
190 return (UserActionListImpl) usertrackingService.getUserActionsByTypeAndMediapackageId(type, id, offset, limit);
191 } else if (!StringUtils.isEmpty(type) && !StringUtils.isEmpty(day)) {
192 return (UserActionListImpl) usertrackingService.getUserActionsByTypeAndDay(type, day, offset, limit);
193 } else if (!StringUtils.isEmpty(type)) {
194 return (UserActionListImpl) usertrackingService.getUserActionsByType(type, offset, limit);
195 } else if (!StringUtils.isEmpty(day)) {
196 return (UserActionListImpl) usertrackingService.getUserActionsByDay(day, offset, limit);
197 } else {
198 return (UserActionListImpl) usertrackingService.getUserActions(offset, limit);
199 }
200 } catch (UserTrackingException e) {
201 throw new WebApplicationException(e);
202 }
203 }
204
205
206
207
208 @GET
209 @Produces(MediaType.APPLICATION_JSON)
210 @Path("/actions.json")
211 @RestQuery(
212 name = "actionsasjson",
213 description = "Get user actions by type and day",
214 returnDescription = "The user actions.",
215 restParameters = {
216 @RestParameter(name = "type", description = "The type of the user action", isRequired = false,
217 type = Type.STRING),
218 @RestParameter(name = "day", description = "The day of creation (format: YYYYMMDD)", isRequired = false,
219 type = Type.STRING),
220 @RestParameter(name = "limit", description = "The maximum number of items to return per page",
221 isRequired = false, type = Type.INTEGER),
222 @RestParameter(name = "offset", description = "The page number", isRequired = false, type = Type.INTEGER)
223 },
224 responses = {
225 @RestResponse(responseCode = SC_OK, description = "A JSON representation of the user actions")
226 })
227 public UserActionListImpl getUserActionsAsJson(@QueryParam("id") String id, @QueryParam("type") String type,
228 @QueryParam("day") String day, @QueryParam("limit") int limit, @QueryParam("offset") int offset) {
229 return getUserActionsAsXml(id, type, day, limit, offset);
230 }
231
232 @GET
233 @Produces(MediaType.TEXT_XML)
234 @Path("/stats.xml")
235 @RestQuery(
236 name = "statsasxml",
237 description = "Get the statistics for an episode",
238 returnDescription = "The statistics.",
239 restParameters = {
240 @RestParameter(name = "id", description = "The ID of the single episode to return the statistics for, "
241 + "if it exists", isRequired = false, type = Type.STRING)
242 },
243 responses = {
244 @RestResponse(responseCode = SC_OK, description = "An XML representation of the episode's statistics")
245 })
246 public StatsImpl statsAsXml(@QueryParam("id") String mediapackageId) {
247 StatsImpl s = new StatsImpl();
248 s.setMediapackageId(mediapackageId);
249 try {
250 s.setViews(usertrackingService.getViews(mediapackageId));
251 } catch (UserTrackingException e) {
252 throw new WebApplicationException(e);
253 }
254 return s;
255 }
256
257 @GET
258 @Produces(MediaType.APPLICATION_JSON)
259 @Path("/stats.json")
260 @RestQuery(
261 name = "statsasjson",
262 description = "Get the statistics for an episode",
263 returnDescription = "The statistics.",
264 restParameters = {
265 @RestParameter(name = "id", description = "The ID of the single episode to return the statistics for, "
266 + "if it exists", isRequired = false, type = Type.STRING)
267 },
268 responses = {
269 @RestResponse(responseCode = SC_OK, description = "A JSON representation of the episode's statistics")
270 })
271 public StatsImpl statsAsJson(@QueryParam("id") String mediapackageId) {
272 return statsAsXml(mediapackageId);
273 }
274
275 @GET
276 @Produces(MediaType.TEXT_XML)
277 @Path("/report.xml")
278 @RestQuery(
279 name = "reportasxml",
280 description = "Get a report for a time range",
281 returnDescription = "The report.",
282 restParameters = {
283 @RestParameter(name = "from", description = "The beginning of the time range", isRequired = false,
284 type = Type.STRING),
285 @RestParameter(name = "to", description = "The end of the time range", isRequired = false,
286 type = Type.STRING),
287 @RestParameter(name = "limit", description = "The maximum number of items to return per page",
288 isRequired = false, type = Type.INTEGER),
289 @RestParameter(name = "offset", description = "The page number", isRequired = false,
290 type = Type.INTEGER)
291 },
292 responses = {
293 @RestResponse(responseCode = SC_OK, description = "An XML representation of the report")
294 })
295 public ReportImpl reportAsXml(@QueryParam("from") String from, @QueryParam("to") String to,
296 @QueryParam("offset") int offset, @QueryParam("limit") int limit) {
297
298
299 if (offset < 0 || limit < 0) {
300 throw new WebApplicationException(Status.BAD_REQUEST);
301 }
302
303
304 if (limit == 0) {
305 limit = 10;
306 }
307
308 try {
309 if (from == null && to == null) {
310 return (ReportImpl) usertrackingService.getReport(offset, limit);
311 } else {
312 return (ReportImpl) usertrackingService.getReport(from, to, offset, limit);
313 }
314 } catch (UserTrackingException e) {
315 throw new WebApplicationException(e);
316 } catch (ParseException e) {
317 throw new WebApplicationException(Status.BAD_REQUEST);
318 }
319 }
320
321 @GET
322 @Produces(MediaType.APPLICATION_JSON)
323 @Path("/report.json")
324 @RestQuery(
325 name = "reportasjson",
326 description = "Get a report for a time range",
327 returnDescription = "The report.",
328 restParameters = {
329 @RestParameter(name = "from", description = "The beginning of the time range", isRequired = false,
330 type = Type.STRING),
331 @RestParameter(name = "to", description = "The end of the time range", isRequired = false,
332 type = Type.STRING),
333 @RestParameter(name = "limit", description = "The maximum number of items to return per page",
334 isRequired = false, type = Type.INTEGER),
335 @RestParameter(name = "offset", description = "The page number", isRequired = false, type = Type.INTEGER)
336 },
337 responses = {
338 @RestResponse(responseCode = SC_OK, description = "A JSON representation of the report")
339 })
340 public ReportImpl reportAsJson(@QueryParam("from") String from, @QueryParam("to") String to,
341 @QueryParam("offset") int offset, @QueryParam("limit") int limit) {
342 return reportAsXml(from, to, offset, limit);
343 }
344
345 @PUT
346 @Path("")
347 @Produces(MediaType.TEXT_XML)
348 @RestQuery(
349 name = "add",
350 description = "Record a user action",
351 returnDescription = "An XML representation of the user action",
352 restParameters = {
353 @RestParameter(name = "id", description = "The episode identifier", isRequired = true, type = Type.STRING),
354 @RestParameter(name = "type", description = "The episode identifier", isRequired = true, type = Type.STRING),
355 @RestParameter(name = "in", description = "The beginning of the time range", isRequired = true,
356 type = Type.STRING),
357 @RestParameter(name = "out", description = "The end of the time range", isRequired = false,
358 type = Type.STRING),
359 @RestParameter(name = "playing", description = "Whether the player is currently playing",
360 isRequired = false, type = Type.STRING)
361 },
362 responses = {
363 @RestResponse(responseCode = SC_CREATED, description = "An XML representation of the user action")
364 })
365 public Response addFootprint(@FormParam("id") String mediapackageId, @FormParam("in") String inString,
366 @FormParam("out") String outString, @FormParam("type") String type, @FormParam("playing") String isPlaying,
367 @Context HttpServletRequest request) {
368
369 String sessionId = request.getSession().getId();
370 String userId = securityService.getUser().getUsername();
371
372
373 if (StringUtils.isEmpty(inString)) {
374 throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity("in must be a non null integer")
375 .build());
376 }
377 Integer in = null;
378 try {
379 in = Integer.parseInt(StringUtils.trim(inString));
380 } catch (NumberFormatException e) {
381 throw new WebApplicationException(e,
382 Response.status(Status.BAD_REQUEST).entity("in must be a non null integer").build());
383 }
384
385 Integer out = null;
386 if (StringUtils.isEmpty(outString)) {
387 out = in;
388 } else {
389 try {
390 out = Integer.parseInt(StringUtils.trim(outString));
391 } catch (NumberFormatException e) {
392 throw new WebApplicationException(e,
393 Response.status(Status.BAD_REQUEST).entity("out must be a non null integer").build());
394 }
395 }
396
397
398 String clientIP = request.getHeader("X-FORWARDED-FOR");
399
400 if (clientIP == null) {
401 clientIP = request.getRemoteAddr();
402 }
403 logger.debug("Got client ip: {}", clientIP);
404
405 UserSession s = new UserSessionImpl();
406 s.setSessionId(sessionId);
407 s.setUserIp(clientIP);
408 s.setUserId(userId);
409
410 String userAgent = StringUtils.trimToNull(request.getHeader("User-Agent"));
411 if (userAgent != null && userAgent.length() > 255) {
412 s.setUserAgent(userAgent.substring(0, 255));
413 } else {
414 s.setUserAgent(userAgent);
415 }
416
417 UserActionImpl a = new UserActionImpl();
418 a.setMediapackageId(mediapackageId);
419 a.setSession(s);
420 a.setInpoint(in);
421 a.setOutpoint(out);
422 a.setType(type);
423 a.setIsPlaying(Boolean.valueOf(isPlaying));
424
425 try {
426 if ("FOOTPRINT".equals(type)) {
427 a = (UserActionImpl) usertrackingService.addUserFootprint(a, s);
428 } else {
429 a = (UserActionImpl) usertrackingService.addUserTrackingEvent(a, s);
430 }
431 } catch (UserTrackingException e) {
432 throw new WebApplicationException(e);
433 }
434
435 URI uri;
436 try {
437 uri = new URI(UrlSupport.concat(new String[] { serverUrl, serviceUrl, "action", a.getId().toString(), ".xml" }));
438 } catch (URISyntaxException e) {
439 throw new WebApplicationException(e);
440 }
441 return Response.created(uri).entity(a).build();
442 }
443
444 @GET
445 @Produces(MediaType.TEXT_XML)
446 @Path("/action/{id}.xml")
447 @RestQuery(
448 name = "add",
449 description = "Record a user action",
450 returnDescription = "An XML representation of the user action",
451 pathParameters = {
452 @RestParameter(name = "id", description = "The episode identifier", isRequired = true, type = Type.STRING)
453 },
454 responses = {
455 @RestResponse(responseCode = SC_OK, description = "An XML representation of the user action")
456 })
457 public UserActionImpl getActionAsXml(@PathParam("id") String actionId) {
458 Long id = null;
459 try {
460 id = Long.parseLong(actionId);
461 } catch (NumberFormatException e) {
462 throw new WebApplicationException(e);
463 }
464 try {
465 return (UserActionImpl) usertrackingService.getUserAction(id);
466 } catch (UserTrackingException e) {
467 throw new WebApplicationException(e);
468 } catch (NotFoundException e) {
469 return null;
470 }
471 }
472
473 @GET
474 @Produces(MediaType.APPLICATION_JSON)
475 @Path("/action/{id}.json")
476 @RestQuery(
477 name = "add",
478 description = "Record a user action",
479 returnDescription = "A JSON representation of the user action",
480 pathParameters = {
481 @RestParameter(name = "id", description = "The episode identifier", isRequired = true, type = Type.STRING)
482 },
483 responses = {
484 @RestResponse(responseCode = SC_OK, description = "A JSON representation of the user action")
485 })
486 public UserActionImpl getActionAsJson(@PathParam("id") String actionId) {
487 return getActionAsXml(actionId);
488 }
489
490 @GET
491 @Produces(MediaType.TEXT_XML)
492 @Path("/footprint.xml")
493 @RestQuery(
494 name = "footprintasxml",
495 description = "Gets the 'footprint' action for an episode",
496 returnDescription = "An XML representation of the footprints",
497 restParameters = {
498 @RestParameter(name = "id", description = "The episode identifier", isRequired = false, type = Type.STRING)
499 },
500 responses = {
501 @RestResponse(responseCode = SC_OK, description = "An XML representation of the footprints")
502 })
503 public FootprintsListImpl getFootprintAsXml(@QueryParam("id") String mediapackageId) {
504 String userId = securityService.getUser().getUsername();
505
506
507 if (mediapackageId == null) {
508 throw new WebApplicationException(Status.BAD_REQUEST);
509 }
510
511 try {
512 return (FootprintsListImpl) usertrackingService.getFootprints(mediapackageId, userId);
513 } catch (UserTrackingException e) {
514 throw new WebApplicationException(e);
515 }
516 }
517
518 @GET
519 @Produces(MediaType.APPLICATION_JSON)
520 @Path("/footprint.json")
521 @RestQuery(
522 name = "footprintasxml",
523 description = "Gets the 'footprint' action for an episode",
524 returnDescription = "A JSON representation of the footprints",
525 restParameters = {
526 @RestParameter(name = "id", description = "The episode identifier", isRequired = false, type = Type.STRING)
527 },
528 responses = {
529 @RestResponse(responseCode = SC_OK, description = "A JSON representation of the footprints")
530 })
531 public FootprintsListImpl getFootprintAsJson(@QueryParam("id") String mediapackageId) {
532 return getFootprintAsXml(mediapackageId);
533 }
534
535 @GET
536 @Produces(MediaType.TEXT_PLAIN)
537 @Path("/detailenabled")
538 public Response getUserTrackingEnabled() {
539 return Response.ok(usertrackingService.getUserTrackingEnabled()).build();
540 }
541 }