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.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   * REST Endpoint for User Tracking Service
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"; // set this to the default value initially
109 
110   /**
111    * Method to set the service this REST endpoint uses
112    *
113    * @param service
114    */
115   @Reference
116   public void setService(UserTrackingService service) {
117     this.usertrackingService = service;
118   }
119 
120   /**
121    * Sets the security service
122    *
123    * @param securityService
124    *          the securityService to set
125    */
126   @Reference
127   public void setSecurityService(SecurityService securityService) {
128     this.securityService = securityService;
129   }
130 
131   /**
132    * The method that is called, when the service is activated
133    *
134    * @param cc
135    *          The ComponentContext of this service
136    */
137   @Activate
138   public void activate(ComponentContext cc) {
139     // Get the configured server URL
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    * @return XML with all footprints
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     // Are the values of offset and limit valid?
180     if (offset < 0 || limit < 0) {
181       throw new WebApplicationException(Status.BAD_REQUEST);
182     }
183 
184     // Set default value of limit (max result value)
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    * @return JSON with all footprints
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); // same logic, different @Produces annotation
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); // same logic, different @Produces annotation
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     // Are the values of offset and limit valid?
299     if (offset < 0 || limit < 0) {
300       throw new WebApplicationException(Status.BAD_REQUEST);
301     }
302 
303     // Set default value of limit (max result value)
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); // same logic, different @Produces annotation
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     // Parse the in and out strings, which might be empty (hence, we can't let jax-rs handle them properly)
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     //MH-8616 the connection might be via a proxy
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     //Column length is currently 255, let's limit it to that.
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     // Is the mediapackageId passed
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); // this is the same logic... it's just annotated differently
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 }