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