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.workflow.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_FORBIDDEN;
27  import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
28  import static javax.servlet.http.HttpServletResponse.SC_NO_CONTENT;
29  import static javax.servlet.http.HttpServletResponse.SC_OK;
30  import static javax.servlet.http.HttpServletResponse.SC_UNAUTHORIZED;
31  import static org.opencastproject.util.doc.rest.RestParameter.Type.STRING;
32  import static org.opencastproject.util.doc.rest.RestParameter.Type.TEXT;
33  
34  import org.opencastproject.job.api.JobProducer;
35  import org.opencastproject.mediapackage.MediaPackage;
36  import org.opencastproject.mediapackage.MediaPackageElement;
37  import org.opencastproject.mediapackage.MediaPackageImpl;
38  import org.opencastproject.mediapackage.MediaPackageParser;
39  import org.opencastproject.mediapackage.MediaPackageSupport;
40  import org.opencastproject.rest.AbstractJobProducerEndpoint;
41  import org.opencastproject.rest.RestConstants;
42  import org.opencastproject.security.api.Permissions;
43  import org.opencastproject.security.api.UnauthorizedException;
44  import org.opencastproject.serviceregistry.api.ServiceRegistry;
45  import org.opencastproject.systems.OpencastConstants;
46  import org.opencastproject.util.LocalHashMap;
47  import org.opencastproject.util.NotFoundException;
48  import org.opencastproject.util.UrlSupport;
49  import org.opencastproject.util.doc.rest.RestParameter;
50  import org.opencastproject.util.doc.rest.RestParameter.Type;
51  import org.opencastproject.util.doc.rest.RestQuery;
52  import org.opencastproject.util.doc.rest.RestResponse;
53  import org.opencastproject.util.doc.rest.RestService;
54  import org.opencastproject.workflow.api.JaxbWorkflowInstance;
55  import org.opencastproject.workflow.api.WorkflowDatabaseException;
56  import org.opencastproject.workflow.api.WorkflowDefinition;
57  import org.opencastproject.workflow.api.WorkflowDefinitionImpl;
58  import org.opencastproject.workflow.api.WorkflowDefinitionSet;
59  import org.opencastproject.workflow.api.WorkflowException;
60  import org.opencastproject.workflow.api.WorkflowInstance;
61  import org.opencastproject.workflow.api.WorkflowInstance.WorkflowState;
62  import org.opencastproject.workflow.api.WorkflowOperationHandler;
63  import org.opencastproject.workflow.api.WorkflowParsingException;
64  import org.opencastproject.workflow.api.WorkflowService;
65  import org.opencastproject.workflow.api.WorkflowSetImpl;
66  import org.opencastproject.workflow.api.WorkflowStateException;
67  import org.opencastproject.workflow.api.XmlWorkflowParser;
68  import org.opencastproject.workflow.impl.WorkflowServiceImpl;
69  import org.opencastproject.workflow.impl.WorkflowServiceImpl.HandlerRegistration;
70  import org.opencastproject.workspace.api.Workspace;
71  
72  import com.google.common.util.concurrent.Striped;
73  
74  import org.apache.commons.lang3.StringUtils;
75  import org.apache.commons.lang3.exception.ExceptionUtils;
76  import org.json.simple.JSONArray;
77  import org.json.simple.JSONObject;
78  import org.osgi.service.component.ComponentContext;
79  import org.osgi.service.component.annotations.Activate;
80  import org.osgi.service.component.annotations.Component;
81  import org.osgi.service.component.annotations.Reference;
82  import org.osgi.service.jaxrs.whiteboard.propertytypes.JaxrsResource;
83  import org.slf4j.Logger;
84  import org.slf4j.LoggerFactory;
85  
86  import java.util.HashMap;
87  import java.util.List;
88  import java.util.Map;
89  import java.util.Optional;
90  import java.util.concurrent.locks.Lock;
91  
92  import javax.servlet.http.HttpServletResponse;
93  import javax.ws.rs.DELETE;
94  import javax.ws.rs.FormParam;
95  import javax.ws.rs.GET;
96  import javax.ws.rs.POST;
97  import javax.ws.rs.Path;
98  import javax.ws.rs.PathParam;
99  import javax.ws.rs.Produces;
100 import javax.ws.rs.QueryParam;
101 import javax.ws.rs.WebApplicationException;
102 import javax.ws.rs.core.MediaType;
103 import javax.ws.rs.core.Response;
104 import javax.ws.rs.core.Response.Status;
105 
106 /**
107  * A REST endpoint for the {@link WorkflowService}
108  */
109 @Path("/workflow")
110 @RestService(name = "workflowservice",
111     title = "Workflow Service",
112     abstractText = "This service lists available workflows and starts, stops, suspends and resumes workflow instances.",
113     notes = {
114         "All paths above are relative to the REST endpoint base (something like http://your.server/files)",
115         "If the service is down or not working it will return a status 503, this means the the underlying service is "
116                 + "not working and is either restarting or has failed",
117         "A status code 500 means a general failure has occurred which is not recoverable and was not anticipated. In "
118                 + "other words, there is a bug! You should file an error report with your server logs from the time "
119                 + "when the error occurred: "
120                 + "<a href=\"https://github.com/opencast/opencast/issues\">Opencast Issue Tracker</a>"
121     }
122 )
123 @Component(
124     immediate = true,
125     service = WorkflowRestService.class,
126     property = {
127         "service.description=Workflow REST Endpoint",
128         "opencast.service.type=org.opencastproject.workflow",
129         "opencast.service.path=/workflow",
130         "opencast.service.jobproducer=true"
131     }
132 )
133 @JaxrsResource
134 public class WorkflowRestService extends AbstractJobProducerEndpoint {
135 
136   /** The default number of results returned */
137   private static final int DEFAULT_LIMIT = 20;
138   /** The constant used to negate a querystring parameter. This is only supported on some parameters. */
139   public static final String NEGATE_PREFIX = "-";
140   /** The constant used to switch the direction of the sorting querystring parameter. */
141   public static final String DESCENDING_SUFFIX = "_DESC";
142   /** The logger */
143   private static final Logger logger = LoggerFactory.getLogger(WorkflowRestService.class);
144 
145   /** The default server URL */
146   protected String serverUrl = UrlSupport.DEFAULT_BASE_URL;
147   /** The default service URL */
148   protected String serviceUrl = serverUrl + "/workflow";
149   /** The workflow service instance */
150   private WorkflowService service;
151   /** The service registry */
152   protected ServiceRegistry serviceRegistry = null;
153   /** The workspace */
154   private Workspace workspace;
155 
156   /** Resource lock */
157   private final Striped<Lock> lock = Striped.lazyWeakLock(1024);
158 
159   /**
160    * Callback from the OSGi declarative services to set the service registry.
161    *
162    * @param serviceRegistry
163    *          the service registry
164    */
165   @Reference
166   protected void setServiceRegistry(ServiceRegistry serviceRegistry) {
167     this.serviceRegistry = serviceRegistry;
168   }
169 
170   /**
171    * Sets the workflow service
172    *
173    * @param service
174    *          the workflow service instance
175    */
176   @Reference
177   public void setService(WorkflowService service) {
178     this.service = service;
179   }
180 
181   /**
182    * Callback from the OSGi declarative services to set the workspace.
183    *
184    * @param workspace
185    *          the workspace
186    */
187   @Reference
188   public void setWorkspace(Workspace workspace) {
189     this.workspace = workspace;
190   }
191 
192   /**
193    * OSGI callback for component activation
194    *
195    * @param cc
196    *          the OSGI declarative services component context
197    */
198   @Activate
199   public void activate(ComponentContext cc) {
200     // Get the configured server URL
201     if (cc == null) {
202       serverUrl = UrlSupport.DEFAULT_BASE_URL;
203     } else {
204       String ccServerUrl = cc.getBundleContext().getProperty(OpencastConstants.SERVER_URL_PROPERTY);
205       logger.info("configured server url is {}", ccServerUrl);
206       if (ccServerUrl == null) {
207         serverUrl = UrlSupport.DEFAULT_BASE_URL;
208       } else {
209         serverUrl = ccServerUrl;
210       }
211       serviceUrl = (String) cc.getProperties().get(RestConstants.SERVICE_PATH_PROPERTY);
212     }
213   }
214 
215   @GET
216   @Produces(MediaType.TEXT_PLAIN)
217   @Path("/count")
218   @RestQuery(name = "count",
219       description = "Returns the number of workflow instances in a specific state",
220       returnDescription = "Returns the number of workflow instances in a specific state",
221       restParameters = {
222           @RestParameter(name = "state", isRequired = false, description = "The workflow state", type = STRING)
223       },
224       responses = {
225           @RestResponse(responseCode = SC_OK, description = "The number of workflow instances.")
226       }
227   )
228   public Response getCount(@QueryParam("state") WorkflowInstance.WorkflowState state,
229           @QueryParam("operation") String operation) {
230     try {
231       Long count = service.countWorkflowInstances(state);
232       return Response.ok(count).build();
233     } catch (WorkflowDatabaseException e) {
234       throw new WebApplicationException(e);
235     }
236   }
237 
238   @GET
239   @Path("definitions.json")
240   @Produces(MediaType.APPLICATION_JSON)
241   @RestQuery(name = "definitions",
242       description = "List all available workflow definitions as JSON",
243       returnDescription = "Returns the workflow definitions as JSON",
244       responses = {
245           @RestResponse(responseCode = SC_OK, description = "The workflow definitions.")
246       }
247   )
248   public WorkflowDefinitionSet getWorkflowDefinitionsAsJson() throws Exception {
249     return getWorkflowDefinitionsAsXml();
250   }
251 
252   @GET
253   @Path("definitions.xml")
254   @Produces(MediaType.APPLICATION_XML)
255   @RestQuery(name = "definitions",
256       description = "List all available workflow definitions as XML",
257       returnDescription = "Returns the workflow definitions as XML",
258       responses = {
259           @RestResponse(responseCode = SC_OK, description = "The workflow definitions.")
260       }
261   )
262   public WorkflowDefinitionSet getWorkflowDefinitionsAsXml() throws Exception {
263     List<WorkflowDefinition> list = service.listAvailableWorkflowDefinitions();
264     return new WorkflowDefinitionSet(list);
265   }
266 
267   @GET
268   @Produces(MediaType.APPLICATION_JSON)
269   @Path("definition/{id}.json")
270   @RestQuery(name = "definitionasjson",
271       description = "Returns a single workflow definition",
272       returnDescription = "Returns a JSON representation of the workflow definition with the specified identifier",
273       pathParameters = {
274           @RestParameter(name = "id", isRequired = true, description = "The workflow definition identifier",
275               type = STRING)
276       },
277       responses = {
278           @RestResponse(responseCode = SC_OK, description = "The workflow definition."),
279           @RestResponse(responseCode = SC_NOT_FOUND, description = "Workflow definition not found.")
280       }
281   )
282   public Response getWorkflowDefinitionAsJson(@PathParam("id") String workflowDefinitionId)
283           throws NotFoundException {
284     WorkflowDefinition def;
285     try {
286       def = service.getWorkflowDefinitionById(workflowDefinitionId);
287     } catch (WorkflowDatabaseException e) {
288       throw new WebApplicationException(e);
289     }
290     return Response.ok(def).build();
291   }
292 
293   @GET
294   @Produces(MediaType.TEXT_XML)
295   @Path("definition/{id}.xml")
296   @RestQuery(name = "definitionasxml",
297       description = "Returns a single workflow definition",
298       returnDescription = "Returns an XML representation of the workflow definition with the specified identifier",
299       pathParameters = {
300           @RestParameter(name = "id", isRequired = true, description = "The workflow definition identifier",
301               type = STRING)
302       }, responses = {
303           @RestResponse(responseCode = SC_OK, description = "The workflow definition."),
304           @RestResponse(responseCode = SC_NOT_FOUND, description = "Workflow definition not found.")
305       }
306   )
307   public Response getWorkflowDefinitionAsXml(@PathParam("id") String workflowDefinitionId)
308           throws NotFoundException {
309     return getWorkflowDefinitionAsJson(workflowDefinitionId);
310   }
311 
312   /**
313    * Returns the workflow configuration panel HTML snippet for the workflow definition specified by
314    *
315    * @param definitionId
316    * @return config panel HTML snippet
317    */
318   @GET
319   @Produces(MediaType.TEXT_HTML)
320   @Path("configurationPanel")
321   @RestQuery(name = "configpanel",
322       description = "Get the configuration panel for a specific workflow",
323       returnDescription = "The HTML workflow configuration panel",
324       restParameters = {
325           @RestParameter(name = "definitionId", isRequired = false, description = "The workflow definition identifier",
326               type = STRING)
327       },
328       responses = {
329       @RestResponse(responseCode = SC_OK, description = "The workflow configuration panel.")
330       }
331   )
332   public Response getConfigurationPanel(@QueryParam("definitionId") String definitionId)
333           throws NotFoundException {
334     try {
335       final WorkflowDefinition def = service.getWorkflowDefinitionById(definitionId);
336       final String out = def.getConfigurationPanel();
337       return Response.ok(out).build();
338     } catch (WorkflowDatabaseException e) {
339       throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR);
340     }
341   }
342 
343   @GET
344   @Produces(MediaType.APPLICATION_JSON)
345   @Path("mediaPackage/{id}/hasActiveWorkflows")
346   @RestQuery(name = "hasactiveworkflows",
347       description = "Returns if a media package has active workflows",
348       returnDescription = "Returns wether the media package has active workflows as a boolean.",
349       pathParameters = {
350           @RestParameter(name = "id", isRequired = true, description = "The media package identifier", type = STRING)
351       },
352       responses = {
353           @RestResponse(responseCode = SC_OK, description = "Whether the media package has active workflows.")
354       }
355   )
356   public Response mediaPackageHasActiveWorkflows(@PathParam("id") String mediaPackageId) {
357     try {
358       return Response.ok(Boolean.toString(service.mediaPackageHasActiveWorkflows(mediaPackageId))).build();
359 
360     } catch (WorkflowDatabaseException e) {
361       throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR);
362     }
363   }
364 
365   @GET
366   @Produces(MediaType.APPLICATION_JSON)
367   @Path("mediaPackage/{id}/instances.json")
368   @RestQuery(name = "workflowsofmediapackage",
369       description = "Returns the workflows for a media package",
370       returnDescription = "Returns the workflows that are associated with the media package as JSON.",
371       pathParameters = {
372           @RestParameter(name = "id", isRequired = true, description = "The media package identifier", type = STRING)
373       },
374       responses = {
375           @RestResponse(responseCode = SC_OK, description = "Returns the workflows for a media package.")
376       }
377   )
378   public Response getWorkflowsOfMediaPackage(@PathParam("id") String mediaPackageId) {
379     try {
380       return Response.ok(new WorkflowSetImpl(service.getWorkflowInstancesByMediaPackage(mediaPackageId))).build();
381     } catch (UnauthorizedException e) {
382       return Response.status(Status.FORBIDDEN).build();
383     } catch (WorkflowDatabaseException e) {
384       throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR);
385     }
386   }
387 
388   @GET
389   @Produces(MediaType.APPLICATION_JSON)
390   @Path("mediaPackage/{id}/currentInstance.json")
391   @RestQuery(name = "currentworkflowofmediapackage",
392       description = "Returns the current workflow for a media package",
393       returnDescription = "Returns the currentworkflow that are associated with the media package as JSON.",
394       pathParameters = {
395           @RestParameter(name = "id", isRequired = true, description = "The media package identifier", type = STRING) },
396       responses = {
397           @RestResponse(responseCode = SC_OK, description = "Returns the workflows for a media package."),
398           @RestResponse(responseCode = SC_NOT_FOUND, description = "Current workflow not found.")
399       }
400   )
401   public Response getRunningWorkflowOfMediaPackage(@PathParam("id") String mediaPackageId) {
402     try {
403       Optional<WorkflowInstance> optWorkflowInstance = service.
404               getRunningWorkflowInstanceByMediaPackage(mediaPackageId, Permissions.Action.READ.toString());
405       if (optWorkflowInstance.isPresent()) {
406         return Response.ok(new JaxbWorkflowInstance(optWorkflowInstance.get())).build();
407       } else {
408         return Response.status(Response.Status.NOT_FOUND).build();
409       }
410 
411     } catch (WorkflowException | UnauthorizedException e) {
412       throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR);
413     }
414   }
415 
416   @GET
417   @Produces(MediaType.APPLICATION_JSON)
418   @Path("user/{id}/hasActiveWorkflows")
419   @RestQuery(name = "userhasactiveworkflows",
420       description = "Returns if there are currently workflow(s) running that were started by the given user",
421       returnDescription = "Returns if there are currently workflow(s) running that were started by the given user "
422               + "as a boolean.",
423       pathParameters = {
424               @RestParameter(name = "id", isRequired = true, description = "The user identifier", type = STRING)
425       },
426       responses = {
427               @RestResponse(responseCode = SC_OK, description = "Whether there are active workflow for the user.")
428       }
429   )
430   public Response userHasActiveWorkflows(@PathParam("id") String userId) {
431     try {
432       return Response.ok(Boolean.toString(service.userHasActiveWorkflows(userId))).build();
433 
434     } catch (WorkflowDatabaseException e) {
435       throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR);
436     }
437   }
438 
439   @GET
440   @Produces(MediaType.TEXT_XML)
441   @Path("instance/{id}.xml")
442   @RestQuery(name = "workflowasxml",
443       description = "Get a specific workflow instance.",
444       returnDescription = "An XML representation of a workflow instance",
445       pathParameters = {
446           @RestParameter(name = "id", isRequired = true, description = "The workflow instance identifier",
447               type = STRING)
448       },
449       responses = {
450           @RestResponse(responseCode = SC_OK, description = "An XML representation of the workflow instance."),
451           @RestResponse(responseCode = SC_NOT_FOUND, description = "No workflow instance with that identifier exists.")
452       }
453   )
454   public JaxbWorkflowInstance getWorkflowAsXml(@PathParam("id") long id) throws WorkflowDatabaseException,
455           NotFoundException, UnauthorizedException {
456     return new JaxbWorkflowInstance(service.getWorkflowById(id));
457   }
458 
459   @GET
460   @Produces(MediaType.APPLICATION_JSON)
461   @Path("instance/{id}.json")
462   @RestQuery(name = "workflowasjson",
463       description = "Get a specific workflow instance.",
464       returnDescription = "A JSON representation of a workflow instance",
465       pathParameters = {
466           @RestParameter(name = "id", isRequired = true, description = "The workflow instance identifier",
467               type = STRING)
468       },
469       responses = {
470           @RestResponse(responseCode = SC_OK, description = "A JSON representation of the workflow instance."),
471           @RestResponse(responseCode = SC_NOT_FOUND, description = "No workflow instance with that identifier exists.")
472       }
473   )
474   public JaxbWorkflowInstance getWorkflowAsJson(@PathParam("id") long id) throws WorkflowDatabaseException,
475           NotFoundException, UnauthorizedException {
476     return getWorkflowAsXml(id);
477   }
478 
479   @POST
480   @Path("start")
481   @Produces(MediaType.TEXT_XML)
482   @RestQuery(name = "start",
483       description = "Start a new workflow instance.",
484       returnDescription = "An XML representation of the new workflow instance",
485       restParameters = {
486           @RestParameter(name = "definition", isRequired = true, description = "The workflow definition ID or an XML "
487               + "representation of a workflow definition", type = TEXT, jaxbClass = WorkflowDefinitionImpl.class),
488           @RestParameter(name = "mediapackage", isRequired = true, description = "The XML representation of a "
489               + "mediapackage", type = TEXT, jaxbClass = MediaPackageImpl.class),
490           @RestParameter(name = "parent", isRequired = false, description = "An optional parent workflow instance "
491               + "identifier", type = STRING),
492           @RestParameter(name = "properties", isRequired = false, description = "An optional set of key=value\\n "
493               + "properties", type = TEXT) }, responses = {
494           @RestResponse(responseCode = SC_OK, description = "An XML representation of the new workflow instance."),
495           @RestResponse(responseCode = SC_UNAUTHORIZED, description = "You do not have permission to resume. Maybe you "
496               + "need to authenticate."),
497           @RestResponse(responseCode = SC_NOT_FOUND, description = "If the parent workflow does not exist")
498       }
499   )
500   public JaxbWorkflowInstance start(@FormParam("definition") String workflowDefinitionXmlOrId,
501           @FormParam("mediapackage") MediaPackageImpl mp, @FormParam("parent") String parentWorkflowId,
502           @FormParam("properties") LocalHashMap localMap) {
503     if (mp == null || StringUtils.isBlank(workflowDefinitionXmlOrId)) {
504       throw new WebApplicationException(Status.BAD_REQUEST);
505     }
506 
507     WorkflowDefinition workflowDefinition;
508     try {
509       workflowDefinition = service.getWorkflowDefinitionById(workflowDefinitionXmlOrId);
510     } catch (Exception e) {
511       // Not an ID. Let's try if it's an XML definition
512       try {
513         workflowDefinition = XmlWorkflowParser.parseWorkflowDefinition(workflowDefinitionXmlOrId);
514       } catch (WorkflowParsingException wpe) {
515         throw new WebApplicationException(wpe, Status.BAD_REQUEST);
516       }
517     }
518 
519     WorkflowInstance instance = null;
520     try {
521       instance = startWorkflow(workflowDefinition, mp, parentWorkflowId, localMap);
522     } catch (UnauthorizedException e) {
523       throw new WebApplicationException(e, Status.UNAUTHORIZED);
524     }
525     return new JaxbWorkflowInstance(instance);
526   }
527 
528   private WorkflowInstance startWorkflow(WorkflowDefinition workflowDefinition, MediaPackageImpl mp,
529           String parentWorkflowId, LocalHashMap localMap) throws UnauthorizedException {
530     Map<String, String> properties = new HashMap<String, String>();
531     if (localMap != null) {
532       properties = localMap.getMap();
533     }
534 
535     Long parentIdAsLong = null;
536     if (StringUtils.isNotEmpty(parentWorkflowId)) {
537       try {
538         parentIdAsLong = Long.parseLong(parentWorkflowId);
539       } catch (NumberFormatException e) {
540         throw new WebApplicationException(e, Status.BAD_REQUEST);
541       }
542     }
543 
544     try {
545       return (WorkflowInstance) service.start(workflowDefinition, mp, parentIdAsLong, properties);
546     } catch (WorkflowException e) {
547       throw new WebApplicationException(e);
548     } catch (NotFoundException e) {
549       throw new WebApplicationException(e);
550     }
551   }
552 
553   @POST
554   @Path("stop")
555   @Produces(MediaType.TEXT_XML)
556   @RestQuery(name = "stop",
557       description = "Stops a workflow instance.",
558       returnDescription = "An XML representation of the stopped workflow instance",
559       restParameters = {
560           @RestParameter(name = "id", isRequired = true, description = "The workflow instance identifier",
561               type = STRING)
562       },
563       responses = {
564           @RestResponse(responseCode = SC_OK, description = "An XML representation of the stopped workflow instance."),
565           @RestResponse(responseCode = SC_NOT_FOUND, description = "No running workflow instance with that identifier "
566               + "exists.")
567       }
568   )
569   public JaxbWorkflowInstance stop(@FormParam("id") long workflowInstanceId)
570           throws WorkflowException, NotFoundException, UnauthorizedException {
571     WorkflowInstance instance = service.stop(workflowInstanceId);
572     return new JaxbWorkflowInstance(instance);
573   }
574 
575   @DELETE
576   @Path("remove/{id}")
577   @Produces(MediaType.TEXT_PLAIN)
578   @RestQuery(name = "remove",
579       description = "Danger! Permenantly removes a workflow instance including all its child jobs. In most "
580           + "circumstances, /stop is what you should use.",
581       returnDescription = "HTTP 204 No Content",
582       pathParameters = {
583           @RestParameter(name = "id", isRequired = true, description = "The workflow instance identifier",
584               type = STRING)
585       },
586       restParameters = {
587           @RestParameter(name = "force", isRequired = false, description = "If the workflow status should be ignored "
588               + "and the workflow removed anyway", type = Type.BOOLEAN, defaultValue = "false")
589       },
590       responses = {
591           @RestResponse(responseCode = HttpServletResponse.SC_NO_CONTENT, description = "If workflow instance could be "
592               + "removed successfully, no content is returned"),
593           @RestResponse(responseCode = SC_NOT_FOUND, description = "No workflow instance with that identifier exists."),
594           @RestResponse(responseCode = SC_FORBIDDEN, description = "It's not allowed to remove other workflow instance "
595               + "statues than STOPPED, SUCCEEDED and FAILED (use force parameter to override AT YOUR OWN RISK).")
596       }
597   )
598   public Response remove(@PathParam("id") long workflowInstanceId, @QueryParam("force") boolean force)
599           throws WorkflowException, NotFoundException, UnauthorizedException {
600     try {
601       service.remove(workflowInstanceId, force);
602     } catch (WorkflowStateException e) {
603       return Response.status(Status.FORBIDDEN).build();
604     }
605     return Response.noContent().build();
606   }
607 
608   @POST
609   @Path("suspend")
610   @Produces(MediaType.TEXT_XML)
611   @RestQuery(name = "suspend",
612       description = "Suspends a workflow instance.",
613       returnDescription = "An XML representation of the suspended workflow instance",
614       restParameters = {
615           @RestParameter(name = "id", isRequired = true, description = "The workflow instance identifier",
616               type = STRING)
617       },
618       responses = {
619           @RestResponse(responseCode = SC_OK, description = "An XML representation of the suspended workflow "
620               + "instance."),
621           @RestResponse(responseCode = SC_NOT_FOUND, description = "No running workflow instance with that identifier "
622               + "exists.")
623       }
624   )
625   public Response suspend(@FormParam("id") long workflowInstanceId) throws NotFoundException, UnauthorizedException {
626     try {
627       WorkflowInstance workflow = service.suspend(workflowInstanceId);
628       return Response.ok(new JaxbWorkflowInstance(workflow)).build();
629     } catch (WorkflowException e) {
630       throw new WebApplicationException(e);
631     }
632   }
633 
634   @POST
635   @Path("resume")
636   @Produces(MediaType.TEXT_XML)
637   @RestQuery(name = "resume",
638       description = "Resumes a suspended workflow instance.",
639       returnDescription = "An XML representation of the resumed workflow instance",
640       restParameters = {
641           @RestParameter(name = "id", isRequired = true, description = "The workflow instance identifier",
642               type = STRING)
643       },
644       responses = {
645           @RestResponse(responseCode = SC_OK, description = "An XML representation of the resumed workflow instance."),
646           @RestResponse(responseCode = SC_CONFLICT, description = "Can not resume workflow not in paused state"),
647           @RestResponse(responseCode = SC_NOT_FOUND, description = "No suspended workflow instance with that "
648               + "identifier exists."),
649           @RestResponse(responseCode = SC_UNAUTHORIZED, description = "You do not have permission to resume. "
650               + "Maybe you need to authenticate.")
651       }
652   )
653   public Response resume(@FormParam("id") long workflowInstanceId, @FormParam("properties") LocalHashMap properties)
654           throws NotFoundException, UnauthorizedException {
655     return resume(workflowInstanceId, null, properties);
656   }
657 
658   @POST
659   @Path("replaceAndresume")
660   @Produces(MediaType.TEXT_XML)
661   @RestQuery(name = "replaceAndresume",
662       description = "Replaces a suspended workflow instance with an updated version, and resumes the workflow.",
663       returnDescription = "An XML representation of the updated and resumed workflow instance",
664       restParameters = {
665           @RestParameter(name = "id", isRequired = true, description = "The workflow instance identifier",
666               type = STRING),
667           @RestParameter(name = "mediapackage", isRequired = false, description = "The new Mediapackage", type = TEXT),
668           @RestParameter(name = "properties", isRequired = false, description = "Properties", type = TEXT) },
669       responses = {
670           @RestResponse(responseCode = SC_OK, description = "An XML representation of the updated and resumed "
671               + "workflow instance."),
672           @RestResponse(responseCode = SC_CONFLICT, description = "Can not resume workflow not in paused state"),
673           @RestResponse(responseCode = SC_NOT_FOUND, description = "No suspended workflow instance with that "
674               + "identifier exists."),
675           @RestResponse(responseCode = SC_UNAUTHORIZED, description = "You do not have permission to resume. "
676               + "Maybe you need to authenticate.")
677       }
678   )
679   public Response resume(@FormParam("id") long workflowInstanceId,
680           @FormParam("mediapackage") final String mediaPackage, @FormParam("properties") LocalHashMap properties)
681           throws NotFoundException, UnauthorizedException {
682     final Map<String, String> map;
683     if (properties == null) {
684       map = new HashMap<String, String>();
685     } else {
686       map = properties.getMap();
687     }
688     final Lock lock = this.lock.get(workflowInstanceId);
689     lock.lock();
690     try {
691       WorkflowInstance workflow = service.getWorkflowById(workflowInstanceId);
692       if (!WorkflowState.PAUSED.equals(workflow.getState())) {
693         logger.warn("Can not resume workflow '{}', not in state paused but {}", workflow, workflow.getState());
694         return Response.status(Status.CONFLICT).build();
695       }
696 
697       if (mediaPackage != null) {
698         MediaPackage newMp = MediaPackageParser.getFromXml(mediaPackage);
699         MediaPackage oldMp = workflow.getMediaPackage();
700 
701         // Delete removed elements from workspace
702         for (MediaPackageElement elem : oldMp.getElements()) {
703           if (MediaPackageSupport.contains(elem.getIdentifier(), newMp)) {
704             continue;
705           }
706           try {
707             workspace.delete(elem.getURI());
708             logger.info("Deleted removed mediapackge element {}", elem);
709           } catch (NotFoundException e) {
710             logger.info("Removed mediapackage element {} is already deleted", elem);
711           }
712         }
713 
714         workflow.setMediaPackage(newMp);
715         service.update(workflow);
716       }
717       workflow = service.resume(workflowInstanceId, map);
718       return Response.ok(new JaxbWorkflowInstance(workflow)).build();
719     } catch (NotFoundException e) {
720       return Response.status(Status.NOT_FOUND).build();
721     } catch (UnauthorizedException e) {
722       return Response.status(Status.UNAUTHORIZED).build();
723     } catch (IllegalStateException e) {
724       logger.warn(ExceptionUtils.getMessage(e));
725       return Response.status(Status.CONFLICT).build();
726     } catch (WorkflowException e) {
727       logger.error(ExceptionUtils.getMessage(e), e);
728       return Response.serverError().build();
729     } catch (Exception e) {
730       logger.error(ExceptionUtils.getMessage(e), e);
731       return Response.serverError().build();
732     }
733     finally {
734       lock.unlock();
735     }
736   }
737 
738   @POST
739   @Path("update")
740   @RestQuery(name = "update",
741       description = "Updates a workflow instance.",
742       returnDescription = "No content.",
743       restParameters = {
744           @RestParameter(name = "workflow", isRequired = true, description = "The XML representation of the "
745               + "workflow instance.", type = TEXT)
746       },
747       responses = {
748           @RestResponse(responseCode = SC_NO_CONTENT, description = "Workflow instance updated.")
749       }
750   )
751   public Response update(@FormParam("workflow") String workflowInstance)
752           throws NotFoundException, UnauthorizedException {
753     try {
754       WorkflowInstance instance = XmlWorkflowParser.parseWorkflowInstance(workflowInstance);
755       service.update(instance);
756       return Response.noContent().build();
757     } catch (WorkflowException e) {
758       throw new WebApplicationException(e);
759     }
760   }
761 
762   @GET
763   @Path("handlers.json")
764   @SuppressWarnings("unchecked")
765   @RestQuery(name = "handlers",
766       description = "List all registered workflow operation handlers (implementations).",
767       returnDescription = "A JSON representation of the registered workflow operation handlers.",
768       responses = {
769           @RestResponse(responseCode = SC_OK, description = "A JSON representation of the registered workflow "
770               + "operation handlers")
771       }
772   )
773   public Response getOperationHandlers() {
774     JSONArray jsonArray = new JSONArray();
775     for (HandlerRegistration reg : ((WorkflowServiceImpl) service).getRegisteredHandlers()) {
776       WorkflowOperationHandler handler = reg.getHandler();
777       JSONObject jsonHandler = new JSONObject();
778       jsonHandler.put("id", handler.getId());
779       jsonHandler.put("description", handler.getDescription());
780       jsonArray.add(jsonHandler);
781     }
782     return Response.ok(jsonArray.toJSONString()).header("Content-Type", MediaType.APPLICATION_JSON).build();
783   }
784 
785   @GET
786   @Path("statemappings.json")
787   @SuppressWarnings("unchecked")
788   @RestQuery(name = "statemappings",
789       description = "Get all workflow state mappings",
790       returnDescription = "A JSON representation of the workflow state mappings.",
791       responses = {
792           @RestResponse(responseCode = SC_OK, description = "A JSON representation of the workflow state mappings")
793       }
794   )
795   public Response getStateMappings() {
796     return Response.ok(new JSONObject(service.getWorkflowStateMappings()).toJSONString())
797         .header("Content-Type", MediaType.APPLICATION_JSON).build();
798   }
799 
800   @Path("/cleanup")
801   @RestQuery(name = "cleanup",
802       description = "Cleans up workflow instances",
803       returnDescription = "No return value",
804       responses = {
805           @RestResponse(responseCode = SC_OK, description = "Cleanup OK"),
806           @RestResponse(responseCode = SC_BAD_REQUEST, description = "Couldn't parse given state"),
807           @RestResponse(responseCode = SC_UNAUTHORIZED, description = "You do not have permission to cleanup. "
808               + "Maybe you need to authenticate."),
809           @RestResponse(responseCode = SC_FORBIDDEN, description = "It's not allowed to delete other workflow "
810               + "instance statues than STOPPED, SUCCEEDED and FAILED")
811       }, restParameters = {
812           @RestParameter(name = "buffer", type = Type.INTEGER, defaultValue = "30", isRequired = true,
813               description = "Lifetime (buffer) in days a workflow instance should live"),
814           @RestParameter(name = "state", type = Type.STRING, isRequired = true,
815               description = "Workflow instance state, only STOPPED, SUCCEEDED and FAILED are allowed values here")
816       }
817   )
818   public Response cleanup(@FormParam("buffer") int buffer, @FormParam("state") String stateParam)
819           throws UnauthorizedException {
820 
821     WorkflowInstance.WorkflowState state;
822     try {
823       state = WorkflowInstance.WorkflowState.valueOf(stateParam);
824     } catch (IllegalArgumentException e) {
825       return Response.status(Status.BAD_REQUEST).build();
826     }
827 
828     if (state != WorkflowInstance.WorkflowState.SUCCEEDED && state != WorkflowInstance.WorkflowState.FAILED
829             && state != WorkflowInstance.WorkflowState.STOPPED) {
830       return Response.status(Status.FORBIDDEN).build();
831     }
832 
833     try {
834       service.cleanupWorkflowInstances(buffer, state);
835       return Response.ok().build();
836     } catch (WorkflowDatabaseException e) {
837       throw new WebApplicationException(e);
838     }
839   }
840 
841   /**
842    * {@inheritDoc}
843    *
844    * @see org.opencastproject.rest.AbstractJobProducerEndpoint#getService()
845    */
846   @Override
847   public JobProducer getService() {
848     if (service instanceof JobProducer) {
849       return (JobProducer) service;
850     } else {
851       return null;
852     }
853   }
854 
855   /**
856    * {@inheritDoc}
857    *
858    * @see org.opencastproject.rest.AbstractJobProducerEndpoint#getServiceRegistry()
859    */
860   @Override
861   public ServiceRegistry getServiceRegistry() {
862     return serviceRegistry;
863   }
864 }