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", title = "Workflow Service", abstractText = "This service lists available workflows and starts, stops, suspends and resumes workflow instances.", notes = {
111         "All paths above are relative to the REST endpoint base (something like http://your.server/files)",
112         "If the service is down or not working it will return a status 503, this means the the underlying service is "
113                 + "not working and is either restarting or has failed",
114         "A status code 500 means a general failure has occurred which is not recoverable and was not anticipated. In "
115                 + "other words, there is a bug! You should file an error report with your server logs from the time when the "
116                 + "error occurred: <a href=\"https://github.com/opencast/opencast/issues\">Opencast Issue Tracker</a>" })
117 @Component(
118     immediate = true,
119     service = WorkflowRestService.class,
120     property = {
121         "service.description=Workflow REST Endpoint",
122         "opencast.service.type=org.opencastproject.workflow",
123         "opencast.service.path=/workflow",
124         "opencast.service.jobproducer=true"
125     }
126 )
127 @JaxrsResource
128 public class WorkflowRestService extends AbstractJobProducerEndpoint {
129 
130   /** The default number of results returned */
131   private static final int DEFAULT_LIMIT = 20;
132   /** The constant used to negate a querystring parameter. This is only supported on some parameters. */
133   public static final String NEGATE_PREFIX = "-";
134   /** The constant used to switch the direction of the sorting querystring parameter. */
135   public static final String DESCENDING_SUFFIX = "_DESC";
136   /** The logger */
137   private static final Logger logger = LoggerFactory.getLogger(WorkflowRestService.class);
138 
139   /** The default server URL */
140   protected String serverUrl = UrlSupport.DEFAULT_BASE_URL;
141   /** The default service URL */
142   protected String serviceUrl = serverUrl + "/workflow";
143   /** The workflow service instance */
144   private WorkflowService service;
145   /** The service registry */
146   protected ServiceRegistry serviceRegistry = null;
147   /** The workspace */
148   private Workspace workspace;
149 
150   /** Resource lock */
151   private final Striped<Lock> lock = Striped.lazyWeakLock(1024);
152 
153   /**
154    * Callback from the OSGi declarative services to set the service registry.
155    *
156    * @param serviceRegistry
157    *          the service registry
158    */
159   @Reference
160   protected void setServiceRegistry(ServiceRegistry serviceRegistry) {
161     this.serviceRegistry = serviceRegistry;
162   }
163 
164   /**
165    * Sets the workflow service
166    *
167    * @param service
168    *          the workflow service instance
169    */
170   @Reference
171   public void setService(WorkflowService service) {
172     this.service = service;
173   }
174 
175   /**
176    * Callback from the OSGi declarative services to set the workspace.
177    *
178    * @param workspace
179    *          the workspace
180    */
181   @Reference
182   public void setWorkspace(Workspace workspace) {
183     this.workspace = workspace;
184   }
185 
186   /**
187    * OSGI callback for component activation
188    *
189    * @param cc
190    *          the OSGI declarative services component context
191    */
192   @Activate
193   public void activate(ComponentContext cc) {
194     // Get the configured server URL
195     if (cc == null) {
196       serverUrl = UrlSupport.DEFAULT_BASE_URL;
197     } else {
198       String ccServerUrl = cc.getBundleContext().getProperty(OpencastConstants.SERVER_URL_PROPERTY);
199       logger.info("configured server url is {}", ccServerUrl);
200       if (ccServerUrl == null) {
201         serverUrl = UrlSupport.DEFAULT_BASE_URL;
202       } else {
203         serverUrl = ccServerUrl;
204       }
205       serviceUrl = (String) cc.getProperties().get(RestConstants.SERVICE_PATH_PROPERTY);
206     }
207   }
208 
209   @GET
210   @Produces(MediaType.TEXT_PLAIN)
211   @Path("/count")
212   @RestQuery(name = "count", description = "Returns the number of workflow instances in a specific state", returnDescription = "Returns the number of workflow instances in a specific state", restParameters = {
213           @RestParameter(name = "state", isRequired = false, description = "The workflow state", type = STRING)},
214           responses = { @RestResponse(responseCode = SC_OK, description = "The number of workflow instances.") })
215   public Response getCount(@QueryParam("state") WorkflowInstance.WorkflowState state,
216           @QueryParam("operation") String operation) {
217     try {
218       Long count = service.countWorkflowInstances(state);
219       return Response.ok(count).build();
220     } catch (WorkflowDatabaseException e) {
221       throw new WebApplicationException(e);
222     }
223   }
224 
225   @GET
226   @Path("definitions.json")
227   @Produces(MediaType.APPLICATION_JSON)
228   @RestQuery(name = "definitions", description = "List all available workflow definitions as JSON", returnDescription = "Returns the workflow definitions as JSON", responses = { @RestResponse(responseCode = SC_OK, description = "The workflow definitions.") })
229   public WorkflowDefinitionSet getWorkflowDefinitionsAsJson() throws Exception {
230     return getWorkflowDefinitionsAsXml();
231   }
232 
233   @GET
234   @Path("definitions.xml")
235   @Produces(MediaType.APPLICATION_XML)
236   @RestQuery(name = "definitions", description = "List all available workflow definitions as XML", returnDescription = "Returns the workflow definitions as XML", responses = { @RestResponse(responseCode = SC_OK, description = "The workflow definitions.") })
237   public WorkflowDefinitionSet getWorkflowDefinitionsAsXml() throws Exception {
238     List<WorkflowDefinition> list = service.listAvailableWorkflowDefinitions();
239     return new WorkflowDefinitionSet(list);
240   }
241 
242   @GET
243   @Produces(MediaType.APPLICATION_JSON)
244   @Path("definition/{id}.json")
245   @RestQuery(name = "definitionasjson", description = "Returns a single workflow definition", returnDescription = "Returns a JSON representation of the workflow definition with the specified identifier", pathParameters = { @RestParameter(name = "id", isRequired = true, description = "The workflow definition identifier", type = STRING) }, responses = {
246           @RestResponse(responseCode = SC_OK, description = "The workflow definition."),
247           @RestResponse(responseCode = SC_NOT_FOUND, description = "Workflow definition not found.") })
248   public Response getWorkflowDefinitionAsJson(@PathParam("id") String workflowDefinitionId)
249           throws NotFoundException {
250     WorkflowDefinition def;
251     try {
252       def = service.getWorkflowDefinitionById(workflowDefinitionId);
253     } catch (WorkflowDatabaseException e) {
254       throw new WebApplicationException(e);
255     }
256     return Response.ok(def).build();
257   }
258 
259   @GET
260   @Produces(MediaType.TEXT_XML)
261   @Path("definition/{id}.xml")
262   @RestQuery(name = "definitionasxml", description = "Returns a single workflow definition", returnDescription = "Returns an XML representation of the workflow definition with the specified identifier", pathParameters = { @RestParameter(name = "id", isRequired = true, description = "The workflow definition identifier", type = STRING) }, responses = {
263           @RestResponse(responseCode = SC_OK, description = "The workflow definition."),
264           @RestResponse(responseCode = SC_NOT_FOUND, description = "Workflow definition not found.") })
265   public Response getWorkflowDefinitionAsXml(@PathParam("id") String workflowDefinitionId)
266           throws NotFoundException {
267     return getWorkflowDefinitionAsJson(workflowDefinitionId);
268   }
269 
270   /**
271    * Returns the workflow configuration panel HTML snippet for the workflow definition specified by
272    *
273    * @param definitionId
274    * @return config panel HTML snippet
275    */
276   @GET
277   @Produces(MediaType.TEXT_HTML)
278   @Path("configurationPanel")
279   @RestQuery(name = "configpanel", description = "Get the configuration panel for a specific workflow", returnDescription = "The HTML workflow configuration panel", restParameters = { @RestParameter(name = "definitionId", isRequired = false, description = "The workflow definition identifier", type = STRING) }, responses = { @RestResponse(responseCode = SC_OK, description = "The workflow configuration panel.") })
280   public Response getConfigurationPanel(@QueryParam("definitionId") String definitionId)
281           throws NotFoundException {
282     try {
283       final WorkflowDefinition def = service.getWorkflowDefinitionById(definitionId);
284       final String out = def.getConfigurationPanel();
285       return Response.ok(out).build();
286     } catch (WorkflowDatabaseException e) {
287       throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR);
288     }
289   }
290 
291   @GET
292   @Produces(MediaType.APPLICATION_JSON)
293   @Path("mediaPackage/{id}/hasActiveWorkflows")
294   @RestQuery(name = "hasactiveworkflows", description = "Returns if a media package has active workflows",
295           returnDescription = "Returns wether the media package has active workflows as a boolean.",
296           pathParameters = {
297                   @RestParameter(name = "id", isRequired = true, description = "The media package identifier", type = STRING) },
298           responses = {
299                   @RestResponse(responseCode = SC_OK, description = "Whether the media package has active workflows.")})
300   public Response mediaPackageHasActiveWorkflows(@PathParam("id") String mediaPackageId) {
301     try {
302       return Response.ok(Boolean.toString(service.mediaPackageHasActiveWorkflows(mediaPackageId))).build();
303 
304     } catch (WorkflowDatabaseException e) {
305       throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR);
306     }
307   }
308 
309   @GET
310   @Produces(MediaType.APPLICATION_JSON)
311   @Path("mediaPackage/{id}/instances.json")
312   @RestQuery(name = "workflowsofmediapackage", description = "Returns the workflows for a media package",
313           returnDescription = "Returns the workflows that are associated with the media package as JSON.",
314           pathParameters = {
315                   @RestParameter(name = "id", isRequired = true, description = "The media package identifier", type = STRING) },
316           responses = {
317                   @RestResponse(responseCode = SC_OK, description = "Returns the workflows for a media package.")})
318   public Response getWorkflowsOfMediaPackage(@PathParam("id") String mediaPackageId) {
319     try {
320       return Response.ok(new WorkflowSetImpl(service.getWorkflowInstancesByMediaPackage(mediaPackageId))).build();
321     } catch (UnauthorizedException e) {
322       return Response.status(Status.FORBIDDEN).build();
323     } catch (WorkflowDatabaseException e) {
324       throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR);
325     }
326   }
327 
328   @GET
329   @Produces(MediaType.APPLICATION_JSON)
330   @Path("mediaPackage/{id}/currentInstance.json")
331   @RestQuery(name = "currentworkflowofmediapackage", description = "Returns the current workflow for a media package",
332           returnDescription = "Returns the currentworkflow that are associated with the media package as JSON.",
333           pathParameters = {
334                   @RestParameter(name = "id", isRequired = true, description = "The media package identifier", type = STRING) },
335           responses = {
336                   @RestResponse(responseCode = SC_OK, description = "Returns the workflows for a media package."),
337                   @RestResponse(responseCode = SC_NOT_FOUND, description = "Current workflow not found.") })
338   public Response getRunningWorkflowOfMediaPackage(@PathParam("id") String mediaPackageId) {
339     try {
340       Optional<WorkflowInstance> optWorkflowInstance = service.
341               getRunningWorkflowInstanceByMediaPackage(mediaPackageId, Permissions.Action.READ.toString());
342       if (optWorkflowInstance.isPresent()) {
343         return Response.ok(new JaxbWorkflowInstance(optWorkflowInstance.get())).build();
344       } else {
345         return Response.status(Response.Status.NOT_FOUND).build();
346       }
347 
348     } catch (WorkflowException | UnauthorizedException e) {
349       throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR);
350     }
351   }
352 
353   @GET
354   @Produces(MediaType.APPLICATION_JSON)
355   @Path("user/{id}/hasActiveWorkflows")
356   @RestQuery(name = "userhasactiveworkflows", description = "Returns if there are currently workflow(s) running that"
357           + "were started by the given user",
358           returnDescription = "Returns if there are currently workflow(s) running that were started by the given user "
359                   + "as a boolean.",
360           pathParameters = {
361                   @RestParameter(name = "id", isRequired = true, description = "The user identifier", type = STRING) },
362           responses = {
363                   @RestResponse(responseCode = SC_OK, description = "Whether there are active workflow for the user.")})
364   public Response userHasActiveWorkflows(@PathParam("id") String userId) {
365     try {
366       return Response.ok(Boolean.toString(service.userHasActiveWorkflows(userId))).build();
367 
368     } catch (WorkflowDatabaseException e) {
369       throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR);
370     }
371   }
372 
373   @GET
374   @Produces(MediaType.TEXT_XML)
375   @Path("instance/{id}.xml")
376   @RestQuery(name = "workflowasxml", description = "Get a specific workflow instance.", returnDescription = "An XML representation of a workflow instance", pathParameters = { @RestParameter(name = "id", isRequired = true, description = "The workflow instance identifier", type = STRING) }, responses = {
377           @RestResponse(responseCode = SC_OK, description = "An XML representation of the workflow instance."),
378           @RestResponse(responseCode = SC_NOT_FOUND, description = "No workflow instance with that identifier exists.") })
379   public JaxbWorkflowInstance getWorkflowAsXml(@PathParam("id") long id) throws WorkflowDatabaseException,
380           NotFoundException, UnauthorizedException {
381     return new JaxbWorkflowInstance(service.getWorkflowById(id));
382   }
383 
384   @GET
385   @Produces(MediaType.APPLICATION_JSON)
386   @Path("instance/{id}.json")
387   @RestQuery(name = "workflowasjson", description = "Get a specific workflow instance.", returnDescription = "A JSON representation of a workflow instance", pathParameters = { @RestParameter(name = "id", isRequired = true, description = "The workflow instance identifier", type = STRING) }, responses = {
388           @RestResponse(responseCode = SC_OK, description = "A JSON representation of the workflow instance."),
389           @RestResponse(responseCode = SC_NOT_FOUND, description = "No workflow instance with that identifier exists.") })
390   public JaxbWorkflowInstance getWorkflowAsJson(@PathParam("id") long id) throws WorkflowDatabaseException,
391           NotFoundException, UnauthorizedException {
392     return getWorkflowAsXml(id);
393   }
394 
395   @POST
396   @Path("start")
397   @Produces(MediaType.TEXT_XML)
398   @RestQuery(name = "start", description = "Start a new workflow instance.", returnDescription = "An XML representation of the new workflow instance", restParameters = {
399           @RestParameter(name = "definition", isRequired = true, description = "The workflow definition ID or an XML representation of a workflow definition", type = TEXT, jaxbClass = WorkflowDefinitionImpl.class),
400           @RestParameter(name = "mediapackage", isRequired = true, description = "The XML representation of a mediapackage", type = TEXT, jaxbClass = MediaPackageImpl.class),
401           @RestParameter(name = "parent", isRequired = false, description = "An optional parent workflow instance identifier", type = STRING),
402           @RestParameter(name = "properties", isRequired = false, description = "An optional set of key=value\\n properties", type = TEXT) }, responses = {
403           @RestResponse(responseCode = SC_OK, description = "An XML representation of the new workflow instance."),
404           @RestResponse(responseCode = SC_UNAUTHORIZED, description = "You do not have permission to resume. Maybe you need to authenticate."),
405           @RestResponse(responseCode = SC_NOT_FOUND, description = "If the parent workflow does not exist") })
406   public JaxbWorkflowInstance start(@FormParam("definition") String workflowDefinitionXmlOrId,
407           @FormParam("mediapackage") MediaPackageImpl mp, @FormParam("parent") String parentWorkflowId,
408           @FormParam("properties") LocalHashMap localMap) {
409     if (mp == null || StringUtils.isBlank(workflowDefinitionXmlOrId))
410       throw new WebApplicationException(Status.BAD_REQUEST);
411 
412     WorkflowDefinition workflowDefinition;
413     try {
414       workflowDefinition = service.getWorkflowDefinitionById(workflowDefinitionXmlOrId);
415     } catch (Exception e) {
416       // Not an ID. Let's try if it's an XML definition
417       try {
418         workflowDefinition = XmlWorkflowParser.parseWorkflowDefinition(workflowDefinitionXmlOrId);
419       } catch (WorkflowParsingException wpe) {
420         throw new WebApplicationException(wpe, Status.BAD_REQUEST);
421       }
422     }
423 
424     WorkflowInstance instance = null;
425     try {
426       instance = startWorkflow(workflowDefinition, mp, parentWorkflowId, localMap);
427     } catch (UnauthorizedException e) {
428       throw new WebApplicationException(e, Status.UNAUTHORIZED);
429     }
430     return new JaxbWorkflowInstance(instance);
431   }
432 
433   private WorkflowInstance startWorkflow(WorkflowDefinition workflowDefinition, MediaPackageImpl mp,
434           String parentWorkflowId, LocalHashMap localMap) throws UnauthorizedException {
435     Map<String, String> properties = new HashMap<String, String>();
436     if (localMap != null)
437       properties = localMap.getMap();
438 
439     Long parentIdAsLong = null;
440     if (StringUtils.isNotEmpty(parentWorkflowId)) {
441       try {
442         parentIdAsLong = Long.parseLong(parentWorkflowId);
443       } catch (NumberFormatException e) {
444         throw new WebApplicationException(e, Status.BAD_REQUEST);
445       }
446     }
447 
448     try {
449       return (WorkflowInstance) service.start(workflowDefinition, mp, parentIdAsLong, properties);
450     } catch (WorkflowException e) {
451       throw new WebApplicationException(e);
452     } catch (NotFoundException e) {
453       throw new WebApplicationException(e);
454     }
455   }
456 
457   @POST
458   @Path("stop")
459   @Produces(MediaType.TEXT_XML)
460   @RestQuery(name = "stop", description = "Stops a workflow instance.", returnDescription = "An XML representation of the stopped workflow instance", restParameters = { @RestParameter(name = "id", isRequired = true, description = "The workflow instance identifier", type = STRING) }, responses = {
461           @RestResponse(responseCode = SC_OK, description = "An XML representation of the stopped workflow instance."),
462           @RestResponse(responseCode = SC_NOT_FOUND, description = "No running workflow instance with that identifier exists.") })
463   public JaxbWorkflowInstance stop(@FormParam("id") long workflowInstanceId) throws WorkflowException, NotFoundException,
464           UnauthorizedException {
465     WorkflowInstance instance = service.stop(workflowInstanceId);
466     return new JaxbWorkflowInstance(instance);
467   }
468 
469   @DELETE
470   @Path("remove/{id}")
471   @Produces(MediaType.TEXT_PLAIN)
472   @RestQuery(name = "remove", description = "Danger! Permenantly removes a workflow instance including all its child jobs. In most circumstances, /stop is what you should use.", returnDescription = "HTTP 204 No Content", pathParameters = {
473           @RestParameter(name = "id", isRequired = true, description = "The workflow instance identifier", type = STRING)}, restParameters = {
474           @RestParameter(name = "force", isRequired = false, description = "If the workflow status should be ignored and the workflow removed anyway", type = Type.BOOLEAN, defaultValue = "false")}, responses = {
475           @RestResponse(responseCode = HttpServletResponse.SC_NO_CONTENT, description = "If workflow instance could be removed successfully, no content is returned"),
476           @RestResponse(responseCode = SC_NOT_FOUND, description = "No workflow instance with that identifier exists."),
477           @RestResponse(responseCode = SC_FORBIDDEN, description = "It's not allowed to remove other workflow instance statues than STOPPED, SUCCEEDED and FAILED (use force parameter to override AT YOUR OWN RISK).") })
478   public Response remove(@PathParam("id") long workflowInstanceId, @QueryParam("force") boolean force) throws WorkflowException, NotFoundException,
479           UnauthorizedException {
480     try {
481       service.remove(workflowInstanceId, force);
482     } catch (WorkflowStateException e) {
483       return Response.status(Status.FORBIDDEN).build();
484     }
485     return Response.noContent().build();
486   }
487 
488   @POST
489   @Path("suspend")
490   @Produces(MediaType.TEXT_XML)
491   @RestQuery(name = "suspend", description = "Suspends a workflow instance.", returnDescription = "An XML representation of the suspended workflow instance", restParameters = { @RestParameter(name = "id", isRequired = true, description = "The workflow instance identifier", type = STRING) }, responses = {
492           @RestResponse(responseCode = SC_OK, description = "An XML representation of the suspended workflow instance."),
493           @RestResponse(responseCode = SC_NOT_FOUND, description = "No running workflow instance with that identifier exists.") })
494   public Response suspend(@FormParam("id") long workflowInstanceId) throws NotFoundException, UnauthorizedException {
495     try {
496       WorkflowInstance workflow = service.suspend(workflowInstanceId);
497       return Response.ok(new JaxbWorkflowInstance(workflow)).build();
498     } catch (WorkflowException e) {
499       throw new WebApplicationException(e);
500     }
501   }
502 
503   @POST
504   @Path("resume")
505   @Produces(MediaType.TEXT_XML)
506   @RestQuery(name = "resume", description = "Resumes a suspended workflow instance.", returnDescription = "An XML representation of the resumed workflow instance", restParameters = { @RestParameter(name = "id", isRequired = true, description = "The workflow instance identifier", type = STRING) }, responses = {
507           @RestResponse(responseCode = SC_OK, description = "An XML representation of the resumed workflow instance."),
508           @RestResponse(responseCode = SC_CONFLICT, description = "Can not resume workflow not in paused state"),
509           @RestResponse(responseCode = SC_NOT_FOUND, description = "No suspended workflow instance with that identifier exists."),
510           @RestResponse(responseCode = SC_UNAUTHORIZED, description = "You do not have permission to resume. Maybe you need to authenticate.") })
511   public Response resume(@FormParam("id") long workflowInstanceId, @FormParam("properties") LocalHashMap properties)
512           throws NotFoundException, UnauthorizedException {
513     return resume(workflowInstanceId, null, properties);
514   }
515 
516   @POST
517   @Path("replaceAndresume")
518   @Produces(MediaType.TEXT_XML)
519   @RestQuery(name = "replaceAndresume", description = "Replaces a suspended workflow instance with an updated version, and resumes the workflow.", returnDescription = "An XML representation of the updated and resumed workflow instance", restParameters = {
520           @RestParameter(name = "id", isRequired = true, description = "The workflow instance identifier", type = STRING),
521           @RestParameter(name = "mediapackage", isRequired = false, description = "The new Mediapackage", type = TEXT),
522           @RestParameter(name = "properties", isRequired = false, description = "Properties", type = TEXT) }, responses = {
523           @RestResponse(responseCode = SC_OK, description = "An XML representation of the updated and resumed workflow instance."),
524           @RestResponse(responseCode = SC_CONFLICT, description = "Can not resume workflow not in paused state"),
525           @RestResponse(responseCode = SC_NOT_FOUND, description = "No suspended workflow instance with that identifier exists."),
526           @RestResponse(responseCode = SC_UNAUTHORIZED, description = "You do not have permission to resume. Maybe you need to authenticate.") })
527   public Response resume(@FormParam("id") long workflowInstanceId,
528           @FormParam("mediapackage") final String mediaPackage, @FormParam("properties") LocalHashMap properties)
529           throws NotFoundException, UnauthorizedException {
530     final Map<String, String> map;
531     if (properties == null) {
532       map = new HashMap<String, String>();
533     } else {
534       map = properties.getMap();
535     }
536     final Lock lock = this.lock.get(workflowInstanceId);
537     lock.lock();
538     try {
539       WorkflowInstance workflow = service.getWorkflowById(workflowInstanceId);
540       if (!WorkflowState.PAUSED.equals(workflow.getState())) {
541         logger.warn("Can not resume workflow '{}', not in state paused but {}", workflow, workflow.getState());
542         return Response.status(Status.CONFLICT).build();
543       }
544 
545       if (mediaPackage != null) {
546         MediaPackage newMp = MediaPackageParser.getFromXml(mediaPackage);
547         MediaPackage oldMp = workflow.getMediaPackage();
548 
549         // Delete removed elements from workspace
550         for (MediaPackageElement elem : oldMp.getElements()) {
551           if (MediaPackageSupport.contains(elem.getIdentifier(), newMp))
552             continue;
553           try {
554             workspace.delete(elem.getURI());
555             logger.info("Deleted removed mediapackge element {}", elem);
556           } catch (NotFoundException e) {
557             logger.info("Removed mediapackage element {} is already deleted", elem);
558           }
559         }
560 
561         workflow.setMediaPackage(newMp);
562         service.update(workflow);
563       }
564       workflow = service.resume(workflowInstanceId, map);
565       return Response.ok(new JaxbWorkflowInstance(workflow)).build();
566     } catch (NotFoundException e) {
567       return Response.status(Status.NOT_FOUND).build();
568     } catch (UnauthorizedException e) {
569       return Response.status(Status.UNAUTHORIZED).build();
570     } catch (IllegalStateException e) {
571       logger.warn(ExceptionUtils.getMessage(e));
572       return Response.status(Status.CONFLICT).build();
573     } catch (WorkflowException e) {
574       logger.error(ExceptionUtils.getMessage(e), e);
575       return Response.serverError().build();
576     } catch (Exception e) {
577       logger.error(ExceptionUtils.getMessage(e), e);
578       return Response.serverError().build();
579     }
580     finally {
581       lock.unlock();
582     }
583   }
584 
585   @POST
586   @Path("update")
587   @RestQuery(name = "update", description = "Updates a workflow instance.", returnDescription = "No content.", restParameters = { @RestParameter(name = "workflow", isRequired = true, description = "The XML representation of the workflow instance.", type = TEXT) }, responses = { @RestResponse(responseCode = SC_NO_CONTENT, description = "Workflow instance updated.") })
588   public Response update(@FormParam("workflow") String workflowInstance) throws NotFoundException,
589           UnauthorizedException {
590     try {
591       WorkflowInstance instance = XmlWorkflowParser.parseWorkflowInstance(workflowInstance);
592       service.update(instance);
593       return Response.noContent().build();
594     } catch (WorkflowException e) {
595       throw new WebApplicationException(e);
596     }
597   }
598 
599   @GET
600   @Path("handlers.json")
601   @SuppressWarnings("unchecked")
602   @RestQuery(name = "handlers", description = "List all registered workflow operation handlers (implementations).", returnDescription = "A JSON representation of the registered workflow operation handlers.", responses = { @RestResponse(responseCode = SC_OK, description = "A JSON representation of the registered workflow operation handlers") })
603   public Response getOperationHandlers() {
604     JSONArray jsonArray = new JSONArray();
605     for (HandlerRegistration reg : ((WorkflowServiceImpl) service).getRegisteredHandlers()) {
606       WorkflowOperationHandler handler = reg.getHandler();
607       JSONObject jsonHandler = new JSONObject();
608       jsonHandler.put("id", handler.getId());
609       jsonHandler.put("description", handler.getDescription());
610       jsonArray.add(jsonHandler);
611     }
612     return Response.ok(jsonArray.toJSONString()).header("Content-Type", MediaType.APPLICATION_JSON).build();
613   }
614 
615   @GET
616   @Path("statemappings.json")
617   @SuppressWarnings("unchecked")
618   @RestQuery(name = "statemappings", description = "Get all workflow state mappings",
619       returnDescription = "A JSON representation of the workflow state mappings.",
620       responses = { @RestResponse(responseCode = SC_OK, description = "A JSON representation of the workflow state mappings") })
621   public Response getStateMappings() {
622     return Response.ok(new JSONObject(service.getWorkflowStateMappings()).toJSONString())
623         .header("Content-Type", MediaType.APPLICATION_JSON).build();
624   }
625 
626   @Path("/cleanup")
627   @RestQuery(name = "cleanup", description = "Cleans up workflow instances", returnDescription = "No return value", responses = {
628           @RestResponse(responseCode = SC_OK, description = "Cleanup OK"),
629           @RestResponse(responseCode = SC_BAD_REQUEST, description = "Couldn't parse given state"),
630           @RestResponse(responseCode = SC_UNAUTHORIZED, description = "You do not have permission to cleanup. Maybe you need to authenticate."),
631           @RestResponse(responseCode = SC_FORBIDDEN, description = "It's not allowed to delete other workflow instance statues than STOPPED, SUCCEEDED and FAILED") }, restParameters = {
632           @RestParameter(name = "buffer", type = Type.INTEGER, defaultValue = "30", isRequired = true, description = "Lifetime (buffer) in days a workflow instance should live"),
633           @RestParameter(name = "state", type = Type.STRING, isRequired = true, description = "Workflow instance state, only STOPPED, SUCCEEDED and FAILED are allowed values here") })
634   public Response cleanup(@FormParam("buffer") int buffer, @FormParam("state") String stateParam)
635           throws UnauthorizedException {
636 
637     WorkflowInstance.WorkflowState state;
638     try {
639       state = WorkflowInstance.WorkflowState.valueOf(stateParam);
640     } catch (IllegalArgumentException e) {
641       return Response.status(Status.BAD_REQUEST).build();
642     }
643 
644     if (state != WorkflowInstance.WorkflowState.SUCCEEDED && state != WorkflowInstance.WorkflowState.FAILED
645             && state != WorkflowInstance.WorkflowState.STOPPED)
646       return Response.status(Status.FORBIDDEN).build();
647 
648     try {
649       service.cleanupWorkflowInstances(buffer, state);
650       return Response.ok().build();
651     } catch (WorkflowDatabaseException e) {
652       throw new WebApplicationException(e);
653     }
654   }
655 
656   /**
657    * {@inheritDoc}
658    *
659    * @see org.opencastproject.rest.AbstractJobProducerEndpoint#getService()
660    */
661   @Override
662   public JobProducer getService() {
663     if (service instanceof JobProducer) {
664       return (JobProducer) service;
665     } else {
666       return null;
667     }
668   }
669 
670   /**
671    * {@inheritDoc}
672    *
673    * @see org.opencastproject.rest.AbstractJobProducerEndpoint#getServiceRegistry()
674    */
675   @Override
676   public ServiceRegistry getServiceRegistry() {
677     return serviceRegistry;
678   }
679 }