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.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
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
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
132 protected IncidentService svc;
133
134
135 protected ServiceRegistry serviceRegistry = null;
136
137
138 protected String serverUrl = UrlSupport.DEFAULT_BASE_URL;
139
140
141 protected String serviceUrl = "/incidents";
142
143
144 @Reference
145 public void setIncidentService(IncidentService incidentService) {
146 this.svc = incidentService;
147 }
148
149
150
151
152
153
154
155 public void activate(ComponentContext cc) {
156
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
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 }