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