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.serviceregistry.impl.endpoint;
23  
24  import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
25  import static javax.servlet.http.HttpServletResponse.SC_CONFLICT;
26  import static javax.servlet.http.HttpServletResponse.SC_CREATED;
27  import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
28  import static javax.servlet.http.HttpServletResponse.SC_OK;
29  import static javax.ws.rs.core.Response.Status.INTERNAL_SERVER_ERROR;
30  import static org.opencastproject.util.EqualsUtil.eq;
31  import static org.opencastproject.util.RestUtil.R.badRequest;
32  import static org.opencastproject.util.RestUtil.R.ok;
33  import static org.opencastproject.util.RestUtil.getResponseType;
34  
35  import org.opencastproject.job.api.Incident;
36  import org.opencastproject.job.api.Incident.Severity;
37  import org.opencastproject.job.api.IncidentTree;
38  import org.opencastproject.job.api.JaxbIncident;
39  import org.opencastproject.job.api.JaxbIncidentDigestList;
40  import org.opencastproject.job.api.JaxbIncidentFullList;
41  import org.opencastproject.job.api.JaxbIncidentFullTree;
42  import org.opencastproject.job.api.JaxbIncidentList;
43  import org.opencastproject.job.api.JaxbIncidentTree;
44  import org.opencastproject.job.api.Job;
45  import org.opencastproject.job.api.JobParser;
46  import org.opencastproject.rest.RestConstants;
47  import org.opencastproject.serviceregistry.api.IncidentL10n;
48  import org.opencastproject.serviceregistry.api.IncidentService;
49  import org.opencastproject.serviceregistry.api.IncidentServiceException;
50  import org.opencastproject.serviceregistry.api.ServiceRegistry;
51  import org.opencastproject.systems.OpencastConstants;
52  import org.opencastproject.util.DateTimeSupport;
53  import org.opencastproject.util.LocalHashMap;
54  import org.opencastproject.util.NotFoundException;
55  import org.opencastproject.util.UrlSupport;
56  import org.opencastproject.util.data.Tuple;
57  import org.opencastproject.util.doc.rest.RestParameter;
58  import org.opencastproject.util.doc.rest.RestParameter.Type;
59  import org.opencastproject.util.doc.rest.RestQuery;
60  import org.opencastproject.util.doc.rest.RestResponse;
61  import org.opencastproject.util.doc.rest.RestService;
62  
63  import org.apache.commons.lang3.LocaleUtils;
64  import org.apache.commons.lang3.StringUtils;
65  import org.json.simple.JSONArray;
66  import org.json.simple.JSONObject;
67  import org.json.simple.JSONValue;
68  import org.osgi.service.component.ComponentContext;
69  import org.osgi.service.component.annotations.Component;
70  import org.osgi.service.component.annotations.Reference;
71  import org.osgi.service.jaxrs.whiteboard.propertytypes.JaxrsResource;
72  import org.slf4j.Logger;
73  import org.slf4j.LoggerFactory;
74  
75  import java.net.URI;
76  import java.util.ArrayList;
77  import java.util.Date;
78  import java.util.HashMap;
79  import java.util.List;
80  import java.util.Map;
81  
82  import javax.servlet.http.HttpServletRequest;
83  import javax.ws.rs.DefaultValue;
84  import javax.ws.rs.FormParam;
85  import javax.ws.rs.GET;
86  import javax.ws.rs.POST;
87  import javax.ws.rs.Path;
88  import javax.ws.rs.PathParam;
89  import javax.ws.rs.Produces;
90  import javax.ws.rs.QueryParam;
91  import javax.ws.rs.WebApplicationException;
92  import javax.ws.rs.core.Context;
93  import javax.ws.rs.core.MediaType;
94  import javax.ws.rs.core.Response;
95  import javax.ws.rs.core.Response.Status;
96  
97  /** REST endpoint for Incident Service. */
98  @Path("/incidents")
99  @RestService(
100     name = "incidentservice",
101     title = "Incident Service",
102     abstractText = "This service creates, edits and retrieves and helps managing incidents.",
103     notes = {
104         "All paths above are relative to the REST endpoint base (something like http://your.server/files)",
105         "If the service is down or not working it will return a status 503, this means the the underlying service is "
106                 + "not working and is either restarting or has failed",
107         "A status code 500 means a general failure has occurred which is not recoverable and was not anticipated. In "
108                 + "other words, there is a bug! You should file an error report with your server logs from the time "
109                 + "when the error occurred: "
110                 + "<a href=\"https://github.com/opencast/opencast/issues\">Opencast Issue Tracker</a>"
111     })
112 @Component(
113   property = {
114     "service.description=Incident Service REST Endpoint",
115     "opencast.service.type=org.opencastproject.incident",
116     "opencast.service.path=/incidents"
117   },
118   immediate = true,
119   service = { IncidentServiceEndpoint.class }
120 )
121 @JaxrsResource
122 public class IncidentServiceEndpoint {
123   /** Logging utility */
124   private static final Logger logger = LoggerFactory.getLogger(IncidentServiceEndpoint.class);
125 
126   public static final String FMT_FULL = "full";
127   public static final String FMT_DIGEST = "digest";
128   public static final String FMT_SYS = "sys";
129   private static final String FMT_DEFAULT = FMT_SYS;
130 
131   /** The incident service */
132   protected IncidentService svc;
133 
134   /** The remote service manager */
135   protected ServiceRegistry serviceRegistry = null;
136 
137   /** This server's base URL */
138   protected String serverUrl = UrlSupport.DEFAULT_BASE_URL;
139 
140   /** The REST endpoint's base URL */
141   protected String serviceUrl = "/incidents";
142 
143   /** OSGi callback for setting the incident service. */
144   @Reference
145   public void setIncidentService(IncidentService incidentService) {
146     this.svc = incidentService;
147   }
148 
149   /**
150    * The method that is called, when the service is activated
151    *
152    * @param cc
153    *         The ComponentContext of this service
154    */
155   public void activate(ComponentContext cc) {
156     // Get the configured server URL
157     if (cc != null) {
158       String ccServerUrl = cc.getBundleContext().getProperty(OpencastConstants.SERVER_URL_PROPERTY);
159       if (StringUtils.isNotBlank(ccServerUrl)) {
160         serverUrl = ccServerUrl;
161       }
162       serviceUrl = (String) cc.getProperties().get(RestConstants.SERVICE_PATH_PROPERTY);
163     }
164   }
165 
166   @GET
167   @Produces({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON})
168   @Path("job/incidents.{type:xml|json}")
169   @RestQuery(
170       name = "incidentsofjobaslist",
171       description = "Returns the job incidents with the given identifiers.",
172       returnDescription = "Returns the job incidents.",
173       pathParameters = {
174          @RestParameter(name = "type", isRequired = true,
175              description = "The media type of the response [xml|json]",
176              defaultValue = "xml",
177              type = Type.STRING)},
178       restParameters = {
179           @RestParameter(name = "id", isRequired = true, description = "The job identifiers.", type = Type.INTEGER),
180           @RestParameter(name = "format", isRequired = false,
181               description = "The response format [full|digest|sys]. Defaults to sys",
182               defaultValue = "sys",
183               type = Type.STRING)
184       },
185       responses = {
186           @RestResponse(responseCode = SC_OK, description = "The job incidents.")
187       })
188   public Response getIncidentsOfJobAsList(
189           @Context HttpServletRequest request,
190           @QueryParam("id") final List<Long> jobIds,
191           @QueryParam("format") @DefaultValue(FMT_DEFAULT) final String format,
192           @PathParam("type") final String type) {
193     try {
194       final List<Incident> incidents = svc.getIncidentsOfJob(jobIds);
195       final MediaType mt = getResponseType(type);
196       if (eq(FMT_SYS, format)) {
197         return ok(mt, new JaxbIncidentList(incidents));
198       } else if (eq(FMT_DIGEST, format)) {
199         return ok(mt, new JaxbIncidentDigestList(svc, request.getLocale(), incidents));
200       } else if (eq(FMT_FULL, format)) {
201         return ok(mt, new JaxbIncidentFullList(svc, request.getLocale(), incidents));
202       } else {
203         return unknownFormat();
204       }
205     } catch (NotFoundException e) {
206       // should not happen
207       logger.error("Unable to get job incident! Consistency issue!", e);
208       throw new WebApplicationException(e, INTERNAL_SERVER_ERROR);
209     } catch (IncidentServiceException e) {
210       logger.warn("Unable to get job incident for id {}: {}", jobIds, e.getMessage());
211       throw new WebApplicationException(INTERNAL_SERVER_ERROR);
212     }
213   }
214 
215   @GET
216   @Produces({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON})
217   @Path("job/{id}.{type:xml|json}")
218   @RestQuery(
219       name = "incidentsofjobastree",
220       description = "Returns the job incident for the job with the given identifier.",
221       returnDescription = "Returns the job incident.",
222       pathParameters = {
223           @RestParameter(name = "id", isRequired = true, description = "The job identifier.", type = Type.INTEGER),
224           @RestParameter(name = "type", isRequired = true,
225               description = "The media type of the response [xml|json]",
226               defaultValue = "xml",
227               type = Type.STRING)},
228       restParameters = {
229           @RestParameter(name = "cascade", isRequired = false, description = "Whether to show the cascaded incidents.",
230               type = Type.BOOLEAN, defaultValue = "false"),
231           @RestParameter(name = "format", isRequired = false,
232               description = "The response format [full|digest|sys]. Defaults to sys",
233               defaultValue = "sys",
234               type = Type.STRING)
235       },
236       responses = {
237              @RestResponse(responseCode = SC_OK, description = "The job incident."),
238              @RestResponse(responseCode = SC_NOT_FOUND, description = "No job incident with this identifier was found.")
239       })
240   public Response getIncidentsOfJobAsTree(
241           @Context HttpServletRequest request,
242           @PathParam("id") final long jobId,
243           @QueryParam("cascade") @DefaultValue("false") boolean cascade,
244           @QueryParam("format") @DefaultValue(FMT_DEFAULT) final String format,
245           @PathParam("type") final String type)
246           throws NotFoundException {
247     try {
248       final IncidentTree tree = svc.getIncidentsOfJob(jobId, cascade);
249       final MediaType mt = getResponseType(type);
250       if (eq(FMT_SYS, format)) {
251         return ok(mt, new JaxbIncidentTree(tree));
252       } else if (eq(FMT_DIGEST, format)) {
253         return ok(mt, new JaxbIncidentFullTree(svc, request.getLocale(), tree));
254       } else if (eq(FMT_FULL, format)) {
255         return ok(mt, new JaxbIncidentFullTree(svc, request.getLocale(), tree));
256       } else {
257         return unknownFormat();
258       }
259     } catch (IncidentServiceException e) {
260       logger.warn("Unable to get job incident for id {}: {}", jobId, e.getMessage());
261       throw new WebApplicationException(INTERNAL_SERVER_ERROR);
262     }
263   }
264 
265   @GET
266   @Produces({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON})
267   @Path("{id}.{type:xml|json}")
268   @RestQuery(
269       name = "incidentjson",
270       description = "Returns the job incident by it's incident identifier.",
271       returnDescription = "Returns the job incident.",
272       pathParameters = {
273           @RestParameter(name = "id", isRequired = true, description = "The incident identifier.", type = Type.INTEGER),
274           @RestParameter(name = "type", isRequired = true,
275               description = "The media type of the response [xml|json]",
276               defaultValue = "xml",
277               type = Type.STRING)},
278       responses = {
279          @RestResponse(responseCode = SC_OK, description = "The job incident."),
280          @RestResponse(responseCode = SC_NOT_FOUND, description = "No job incident with this identifier was found.")
281       })
282   public Response getIncident(@PathParam("id") final long incidentId, @PathParam("type") final String type)
283           throws NotFoundException {
284     try {
285       Incident incident = svc.getIncident(incidentId);
286       return ok(getResponseType(type), new JaxbIncident(incident));
287     } catch (IncidentServiceException e) {
288       logger.warn("Unable to get job incident for incident id {}", incidentId, e);
289       throw new WebApplicationException(INTERNAL_SERVER_ERROR);
290     }
291   }
292 
293   @GET
294   @SuppressWarnings("unchecked")
295   @Produces(MediaType.APPLICATION_JSON)
296   @Path("localization/{id}")
297   @RestQuery(
298       name = "getlocalization",
299       description = "Returns the localization of an incident by it's id as JSON",
300       returnDescription = "The localization of the incident as JSON",
301       pathParameters = {
302           @RestParameter(name = "id", isRequired = true, description = "The incident identifiers.", type = Type.INTEGER)
303       },
304       restParameters = {
305           @RestParameter(name = "locale", isRequired = true, description = "The locale.", type = Type.STRING)
306       },
307       responses = {
308           @RestResponse(responseCode = SC_OK, description = "The localization of the given job incidents."),
309           @RestResponse(responseCode = SC_NOT_FOUND,
310               description = "No job incident with this incident identifier was found.")
311       })
312   public Response getLocalization(@PathParam("id") final long incidentId, @QueryParam("locale") String locale)
313           throws NotFoundException {
314     try {
315       IncidentL10n localization = svc.getLocalization(incidentId, LocaleUtils.toLocale(locale));
316       JSONObject json = new JSONObject();
317       json.put("title", localization.getTitle());
318       json.put("description", localization.getDescription());
319       return Response.ok(json.toJSONString()).build();
320     } catch (IncidentServiceException e) {
321       logger.warn("Unable to get job localization of jo incident:", e);
322       throw new WebApplicationException(INTERNAL_SERVER_ERROR);
323     }
324   }
325 
326   @POST
327   @Produces(MediaType.APPLICATION_XML)
328   @Path("/")
329   @RestQuery(
330       name = "postincident",
331       description = "Creates a new job incident and returns it as XML",
332       returnDescription = "Returns the created job incident as XML",
333       restParameters = {
334           @RestParameter(name = "job", isRequired = true, description = "The job on where to create the incident",
335               type = Type.TEXT),
336           @RestParameter(name = "date", isRequired = true, description = "The incident creation date",
337               type = Type.STRING),
338           @RestParameter(name = "code", isRequired = true, description = "The incident error code",
339               type = Type.STRING),
340           @RestParameter(name = "severity", isRequired = true, description = "The incident error code",
341               type = Type.STRING),
342           @RestParameter(name = "details", isRequired = false, description = "The incident details",
343               type = Type.TEXT),
344           @RestParameter(name = "params", isRequired = false, description = "The incident parameters",
345               type = Type.TEXT)
346       },
347       responses = {
348           @RestResponse(responseCode = SC_CREATED, description = "New job incident has been created"),
349           @RestResponse(responseCode = SC_BAD_REQUEST, description = "Unable to parse the one of the form params"),
350           @RestResponse(responseCode = SC_CONFLICT, description = "No job incident related job exists")
351       })
352   public Response postIncident(@FormParam("job") String jobXml, @FormParam("date") String date,
353                                @FormParam("code") String code, @FormParam("severity") String severityString,
354                                @FormParam("details") String details, @FormParam("params") LocalHashMap params) {
355     Job job;
356     Date timestamp;
357     Severity severity;
358     Map<String, String> map = new HashMap<String, String>();
359     List<Tuple<String, String>> list = new ArrayList<Tuple<String, String>>();
360     try {
361       job = JobParser.parseJob(jobXml);
362       timestamp = new Date(DateTimeSupport.fromUTC(date));
363       severity = Severity.valueOf(severityString);
364       if (params != null) {
365         map = params.getMap();
366       }
367       if (StringUtils.isNotBlank(details)) {
368         final JSONArray array = (JSONArray) JSONValue.parse(details);
369         for (int i = 0; i < array.size(); i++) {
370           JSONObject tuple = (JSONObject) array.get(i);
371           list.add(Tuple.tuple((String) tuple.get("title"), (String) tuple.get("content")));
372         }
373       }
374     } catch (Exception e) {
375       return Response.status(Status.BAD_REQUEST).entity(e.getMessage()).build();
376     }
377 
378     try {
379       Incident incident = svc.storeIncident(job, timestamp, code, severity, map, list);
380       String uri = UrlSupport.concat(serverUrl, serviceUrl, Long.toString(incident.getId()), ".xml");
381       return Response.created(new URI(uri)).entity(new JaxbIncident(incident)).build();
382     } catch (IllegalStateException e) {
383       return Response.status(Status.CONFLICT).build();
384     } catch (Exception e) {
385       logger.warn("Unable to post incident for job {}", job.getId(), e);
386       throw new WebApplicationException(INTERNAL_SERVER_ERROR);
387     }
388   }
389 
390   private Response unknownFormat() {
391     return badRequest("Unknown format");
392   }
393 }