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(name = "usertracking", title = "User Tracking Service", abstractText = "This service is used for tracking user interaction creates, edits and retrieves user actions and "
76 + "viewing statistics.", notes = {
77 "All paths above are relative to the REST endpoint base (something like http://your.server/files)",
78 "If the service is down or not working it will return a status 503, this means the the underlying service is "
79 + "not working and is either restarting or has failed",
80 "A status code 500 means a general failure has occurred which is not recoverable and was not anticipated. In "
81 + "other words, there is a bug! You should file an error report with your server logs from the time when the "
82 + "error occurred: <a href=\"https://github.com/opencast/opencast/issues\">Opencast Issue Tracker</a>" })
83 @Component(
84 immediate = true,
85 service = UserTrackingRestService.class,
86 property = {
87 "service.description=User Tracking REST Endpoint",
88 "opencast.service.type=org.opencastproject.usertracking",
89 "opencast.service.path=/usertracking"
90 }
91 )
92 @JaxrsResource
93 public class UserTrackingRestService {
94
95 private static final Logger logger = LoggerFactory.getLogger(UserTrackingRestService.class);
96
97 private UserTrackingService usertrackingService;
98
99 protected SecurityService securityService;
100
101 protected String serverUrl = UrlSupport.DEFAULT_BASE_URL;
102
103 protected String serviceUrl = "/usertracking";
104
105
106
107
108
109
110 @Reference
111 public void setService(UserTrackingService service) {
112 this.usertrackingService = service;
113 }
114
115
116
117
118
119
120
121 @Reference
122 public void setSecurityService(SecurityService securityService) {
123 this.securityService = securityService;
124 }
125
126
127
128
129
130
131
132 @Activate
133 public void activate(ComponentContext cc) {
134
135 if (cc == null) {
136 serverUrl = UrlSupport.DEFAULT_BASE_URL;
137 } else {
138 String ccServerUrl = cc.getBundleContext().getProperty(OpencastConstants.SERVER_URL_PROPERTY);
139 logger.info("configured server url is {}", ccServerUrl);
140 if (ccServerUrl == null) {
141 serverUrl = UrlSupport.DEFAULT_BASE_URL;
142 } else {
143 serverUrl = ccServerUrl;
144 }
145 serviceUrl = (String) cc.getProperties().get(RestConstants.SERVICE_PATH_PROPERTY);
146 }
147 }
148
149
150
151
152 @GET
153 @Produces(MediaType.TEXT_XML)
154 @Path("/actions.xml")
155 @RestQuery(name = "actionsasxml", description = "Get user actions by type and day", returnDescription = "The user actions.", restParameters = {
156 @RestParameter(name = "type", description = "The type of the user action", isRequired = false, type = Type.STRING),
157 @RestParameter(name = "day", description = "The day of creation (format: YYYYMMDD)", isRequired = false, type = Type.STRING),
158 @RestParameter(name = "limit", description = "The maximum number of items to return per page", isRequired = false, type = Type.INTEGER),
159 @RestParameter(name = "offset", description = "The page number", isRequired = false, type = Type.INTEGER) }, responses = { @RestResponse(responseCode = SC_OK, description = "An XML representation of the user actions") })
160 public UserActionListImpl getUserActionsAsXml(@QueryParam("id") String id, @QueryParam("type") String type,
161 @QueryParam("day") String day, @QueryParam("limit") int limit, @QueryParam("offset") int offset) {
162
163
164 if (offset < 0 || limit < 0)
165 throw new WebApplicationException(Status.BAD_REQUEST);
166
167
168 if (limit == 0)
169 limit = 10;
170 try {
171 if (!StringUtils.isEmpty(id) && !StringUtils.isEmpty(type))
172 return (UserActionListImpl) usertrackingService.getUserActionsByTypeAndMediapackageId(type, id, offset, limit);
173 else if (!StringUtils.isEmpty(type) && !StringUtils.isEmpty(day))
174 return (UserActionListImpl) usertrackingService.getUserActionsByTypeAndDay(type, day, offset, limit);
175 else if (!StringUtils.isEmpty(type))
176 return (UserActionListImpl) usertrackingService.getUserActionsByType(type, offset, limit);
177 else if (!StringUtils.isEmpty(day))
178 return (UserActionListImpl) usertrackingService.getUserActionsByDay(day, offset, limit);
179 else
180 return (UserActionListImpl) usertrackingService.getUserActions(offset, limit);
181 } catch (UserTrackingException e) {
182 throw new WebApplicationException(e);
183 }
184 }
185
186
187
188
189 @GET
190 @Produces(MediaType.APPLICATION_JSON)
191 @Path("/actions.json")
192 @RestQuery(name = "actionsasjson", description = "Get user actions by type and day", returnDescription = "The user actions.", restParameters = {
193 @RestParameter(name = "type", description = "The type of the user action", isRequired = false, type = Type.STRING),
194 @RestParameter(name = "day", description = "The day of creation (format: YYYYMMDD)", isRequired = false, type = Type.STRING),
195 @RestParameter(name = "limit", description = "The maximum number of items to return per page", isRequired = false, type = Type.INTEGER),
196 @RestParameter(name = "offset", description = "The page number", isRequired = false, type = Type.INTEGER) }, responses = { @RestResponse(responseCode = SC_OK, description = "A JSON representation of the user actions") })
197 public UserActionListImpl getUserActionsAsJson(@QueryParam("id") String id, @QueryParam("type") String type,
198 @QueryParam("day") String day, @QueryParam("limit") int limit, @QueryParam("offset") int offset) {
199 return getUserActionsAsXml(id, type, day, limit, offset);
200 }
201
202 @GET
203 @Produces(MediaType.TEXT_XML)
204 @Path("/stats.xml")
205 @RestQuery(name = "statsasxml", description = "Get the statistics for an episode", returnDescription = "The statistics.", restParameters = { @RestParameter(name = "id", description = "The ID of the single episode to return the statistics for, if it exists", isRequired = false, type = Type.STRING) }, responses = { @RestResponse(responseCode = SC_OK, description = "An XML representation of the episode's statistics") })
206 public StatsImpl statsAsXml(@QueryParam("id") String mediapackageId) {
207 StatsImpl s = new StatsImpl();
208 s.setMediapackageId(mediapackageId);
209 try {
210 s.setViews(usertrackingService.getViews(mediapackageId));
211 } catch (UserTrackingException e) {
212 throw new WebApplicationException(e);
213 }
214 return s;
215 }
216
217 @GET
218 @Produces(MediaType.APPLICATION_JSON)
219 @Path("/stats.json")
220 @RestQuery(name = "statsasjson", description = "Get the statistics for an episode", returnDescription = "The statistics.", restParameters = { @RestParameter(name = "id", description = "The ID of the single episode to return the statistics for, if it exists", isRequired = false, type = Type.STRING) }, responses = { @RestResponse(responseCode = SC_OK, description = "A JSON representation of the episode's statistics") })
221 public StatsImpl statsAsJson(@QueryParam("id") String mediapackageId) {
222 return statsAsXml(mediapackageId);
223 }
224
225 @GET
226 @Produces(MediaType.TEXT_XML)
227 @Path("/report.xml")
228 @RestQuery(name = "reportasxml", description = "Get a report for a time range", returnDescription = "The report.", restParameters = {
229 @RestParameter(name = "from", description = "The beginning of the time range", isRequired = false, type = Type.STRING),
230 @RestParameter(name = "to", description = "The end of the time range", isRequired = false, type = Type.STRING),
231 @RestParameter(name = "limit", description = "The maximum number of items to return per page", isRequired = false, type = Type.INTEGER),
232 @RestParameter(name = "offset", description = "The page number", isRequired = false, type = Type.INTEGER) }, responses = { @RestResponse(responseCode = SC_OK, description = "An XML representation of the report") })
233 public ReportImpl reportAsXml(@QueryParam("from") String from, @QueryParam("to") String to,
234 @QueryParam("offset") int offset, @QueryParam("limit") int limit) {
235
236
237 if (offset < 0 || limit < 0)
238 throw new WebApplicationException(Status.BAD_REQUEST);
239
240
241 if (limit == 0)
242 limit = 10;
243
244 try {
245 if (from == null && to == null)
246 return (ReportImpl) usertrackingService.getReport(offset, limit);
247 else
248 return (ReportImpl) usertrackingService.getReport(from, to, offset, limit);
249 } catch (UserTrackingException e) {
250 throw new WebApplicationException(e);
251 } catch (ParseException e) {
252 throw new WebApplicationException(Status.BAD_REQUEST);
253 }
254 }
255
256 @GET
257 @Produces(MediaType.APPLICATION_JSON)
258 @Path("/report.json")
259 @RestQuery(name = "reportasjson", description = "Get a report for a time range", returnDescription = "The report.", restParameters = {
260 @RestParameter(name = "from", description = "The beginning of the time range", isRequired = false, type = Type.STRING),
261 @RestParameter(name = "to", description = "The end of the time range", isRequired = false, type = Type.STRING),
262 @RestParameter(name = "limit", description = "The maximum number of items to return per page", isRequired = false, type = Type.INTEGER),
263 @RestParameter(name = "offset", description = "The page number", isRequired = false, type = Type.INTEGER) }, responses = { @RestResponse(responseCode = SC_OK, description = "A JSON representation of the report") })
264 public ReportImpl reportAsJson(@QueryParam("from") String from, @QueryParam("to") String to,
265 @QueryParam("offset") int offset, @QueryParam("limit") int limit) {
266 return reportAsXml(from, to, offset, limit);
267 }
268
269 @PUT
270 @Path("")
271 @Produces(MediaType.TEXT_XML)
272 @RestQuery(name = "add", description = "Record a user action", returnDescription = "An XML representation of the user action", restParameters = {
273 @RestParameter(name = "id", description = "The episode identifier", isRequired = true, type = Type.STRING),
274 @RestParameter(name = "type", description = "The episode identifier", isRequired = true, type = Type.STRING),
275 @RestParameter(name = "in", description = "The beginning of the time range", isRequired = true, type = Type.STRING),
276 @RestParameter(name = "out", description = "The end of the time range", isRequired = false, type = Type.STRING),
277 @RestParameter(name = "playing", description = "Whether the player is currently playing", isRequired = false, type = Type.STRING)}, responses = { @RestResponse(responseCode = SC_CREATED, description = "An XML representation of the user action") })
278 public Response addFootprint(@FormParam("id") String mediapackageId, @FormParam("in") String inString,
279 @FormParam("out") String outString, @FormParam("type") String type, @FormParam("playing") String isPlaying,
280 @Context HttpServletRequest request) {
281
282 String sessionId = request.getSession().getId();
283 String userId = securityService.getUser().getUsername();
284
285
286 if (StringUtils.isEmpty(inString)) {
287 throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity("in must be a non null integer")
288 .build());
289 }
290 Integer in = null;
291 try {
292 in = Integer.parseInt(StringUtils.trim(inString));
293 } catch (NumberFormatException e) {
294 throw new WebApplicationException(e, Response.status(Status.BAD_REQUEST).entity("in must be a non null integer").build());
295 }
296
297 Integer out = null;
298 if (StringUtils.isEmpty(outString)) {
299 out = in;
300 } else {
301 try {
302 out = Integer.parseInt(StringUtils.trim(outString));
303 } catch (NumberFormatException e) {
304 throw new WebApplicationException(e, Response.status(Status.BAD_REQUEST).entity("out must be a non null integer").build());
305 }
306 }
307
308
309 String clientIP = request.getHeader("X-FORWARDED-FOR");
310
311 if (clientIP == null) {
312 clientIP = request.getRemoteAddr();
313 }
314 logger.debug("Got client ip: {}", clientIP);
315
316 UserSession s = new UserSessionImpl();
317 s.setSessionId(sessionId);
318 s.setUserIp(clientIP);
319 s.setUserId(userId);
320
321 String userAgent = StringUtils.trimToNull(request.getHeader("User-Agent"));
322 if (userAgent != null && userAgent.length() > 255) {
323 s.setUserAgent(userAgent.substring(0, 255));
324 } else {
325 s.setUserAgent(userAgent);
326 }
327
328 UserActionImpl a = new UserActionImpl();
329 a.setMediapackageId(mediapackageId);
330 a.setSession(s);
331 a.setInpoint(in);
332 a.setOutpoint(out);
333 a.setType(type);
334 a.setIsPlaying(Boolean.valueOf(isPlaying));
335
336 try {
337 if ("FOOTPRINT".equals(type)) {
338 a = (UserActionImpl) usertrackingService.addUserFootprint(a, s);
339 } else {
340 a = (UserActionImpl) usertrackingService.addUserTrackingEvent(a, s);
341 }
342 } catch (UserTrackingException e) {
343 throw new WebApplicationException(e);
344 }
345
346 URI uri;
347 try {
348 uri = new URI(UrlSupport.concat(new String[] { serverUrl, serviceUrl, "action", a.getId().toString(), ".xml" }));
349 } catch (URISyntaxException e) {
350 throw new WebApplicationException(e);
351 }
352 return Response.created(uri).entity(a).build();
353 }
354
355 @GET
356 @Produces(MediaType.TEXT_XML)
357 @Path("/action/{id}.xml")
358 @RestQuery(name = "add", description = "Record a user action", returnDescription = "An XML representation of the user action", pathParameters = { @RestParameter(name = "id", description = "The episode identifier", isRequired = true, type = Type.STRING) }, responses = { @RestResponse(responseCode = SC_OK, description = "An XML representation of the user action") })
359 public UserActionImpl getActionAsXml(@PathParam("id") String actionId) {
360 Long id = null;
361 try {
362 id = Long.parseLong(actionId);
363 } catch (NumberFormatException e) {
364 throw new WebApplicationException(e);
365 }
366 try {
367 return (UserActionImpl) usertrackingService.getUserAction(id);
368 } catch (UserTrackingException e) {
369 throw new WebApplicationException(e);
370 } catch (NotFoundException e) {
371 return null;
372 }
373 }
374
375 @GET
376 @Produces(MediaType.APPLICATION_JSON)
377 @Path("/action/{id}.json")
378 @RestQuery(name = "add", description = "Record a user action", returnDescription = "A JSON representation of the user action", pathParameters = { @RestParameter(name = "id", description = "The episode identifier", isRequired = true, type = Type.STRING) }, responses = { @RestResponse(responseCode = SC_OK, description = "A JSON representation of the user action") })
379 public UserActionImpl getActionAsJson(@PathParam("id") String actionId) {
380 return getActionAsXml(actionId);
381 }
382
383 @GET
384 @Produces(MediaType.TEXT_XML)
385 @Path("/footprint.xml")
386 @RestQuery(name = "footprintasxml", description = "Gets the 'footprint' action for an episode", returnDescription = "An XML representation of the footprints", restParameters = { @RestParameter(name = "id", description = "The episode identifier", isRequired = false, type = Type.STRING) }, responses = { @RestResponse(responseCode = SC_OK, description = "An XML representation of the footprints") })
387 public FootprintsListImpl getFootprintAsXml(@QueryParam("id") String mediapackageId) {
388 String userId = securityService.getUser().getUsername();
389
390
391 if (mediapackageId == null)
392 throw new WebApplicationException(Status.BAD_REQUEST);
393
394 try {
395 return (FootprintsListImpl) usertrackingService.getFootprints(mediapackageId, userId);
396 } catch (UserTrackingException e) {
397 throw new WebApplicationException(e);
398 }
399 }
400
401 @GET
402 @Produces(MediaType.APPLICATION_JSON)
403 @Path("/footprint.json")
404 @RestQuery(name = "footprintasxml", description = "Gets the 'footprint' action for an episode", returnDescription = "A JSON representation of the footprints", restParameters = { @RestParameter(name = "id", description = "The episode identifier", isRequired = false, type = Type.STRING) }, responses = { @RestResponse(responseCode = SC_OK, description = "A JSON representation of the footprints") })
405 public FootprintsListImpl getFootprintAsJson(@QueryParam("id") String mediapackageId) {
406 return getFootprintAsXml(mediapackageId);
407 }
408
409 @GET
410 @Produces(MediaType.TEXT_PLAIN)
411 @Path("/detailenabled")
412 public Response getUserTrackingEnabled() {
413 return Response.ok(usertrackingService.getUserTrackingEnabled()).build();
414 }
415 }