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  package org.opencastproject.external.endpoint;
22  
23  import static org.apache.commons.lang3.StringUtils.defaultString;
24  import static org.apache.commons.lang3.exception.ExceptionUtils.getMessage;
25  import static org.opencastproject.index.service.util.JSONUtils.arrayToJsonArray;
26  import static org.opencastproject.index.service.util.JSONUtils.safeString;
27  import static org.opencastproject.util.doc.rest.RestParameter.Type.BOOLEAN;
28  import static org.opencastproject.util.doc.rest.RestParameter.Type.INTEGER;
29  import static org.opencastproject.util.doc.rest.RestParameter.Type.STRING;
30  import static org.opencastproject.util.requests.SortCriterion.Order.Descending;
31  
32  import org.opencastproject.external.common.ApiMediaType;
33  import org.opencastproject.external.common.ApiResponseBuilder;
34  import org.opencastproject.index.service.util.RestUtils;
35  import org.opencastproject.util.NotFoundException;
36  import org.opencastproject.util.RestUtil;
37  import org.opencastproject.util.doc.rest.RestParameter;
38  import org.opencastproject.util.doc.rest.RestQuery;
39  import org.opencastproject.util.doc.rest.RestResponse;
40  import org.opencastproject.util.doc.rest.RestService;
41  import org.opencastproject.util.requests.SortCriterion;
42  import org.opencastproject.workflow.api.RetryStrategy;
43  import org.opencastproject.workflow.api.WorkflowDatabaseException;
44  import org.opencastproject.workflow.api.WorkflowDefinition;
45  import org.opencastproject.workflow.api.WorkflowOperationDefinition;
46  import org.opencastproject.workflow.api.WorkflowService;
47  
48  import com.google.gson.JsonArray;
49  import com.google.gson.JsonObject;
50  
51  import org.apache.commons.collections4.comparators.ComparatorChain;
52  import org.apache.commons.lang3.ArrayUtils;
53  import org.apache.commons.lang3.StringUtils;
54  import org.osgi.service.component.ComponentContext;
55  import org.osgi.service.component.annotations.Activate;
56  import org.osgi.service.component.annotations.Component;
57  import org.osgi.service.component.annotations.Reference;
58  import org.osgi.service.jaxrs.whiteboard.propertytypes.JaxrsResource;
59  import org.slf4j.Logger;
60  import org.slf4j.LoggerFactory;
61  
62  import java.util.ArrayList;
63  import java.util.List;
64  import java.util.stream.Collectors;
65  import java.util.stream.Stream;
66  
67  import javax.servlet.http.HttpServletResponse;
68  import javax.ws.rs.GET;
69  import javax.ws.rs.HeaderParam;
70  import javax.ws.rs.Path;
71  import javax.ws.rs.PathParam;
72  import javax.ws.rs.Produces;
73  import javax.ws.rs.QueryParam;
74  import javax.ws.rs.core.Response;
75  
76  @Path("/api/workflow-definitions")
77  @Produces({ ApiMediaType.JSON, ApiMediaType.VERSION_1_1_0, ApiMediaType.VERSION_1_2_0, ApiMediaType.VERSION_1_3_0,
78              ApiMediaType.VERSION_1_4_0, ApiMediaType.VERSION_1_5_0, ApiMediaType.VERSION_1_6_0,
79              ApiMediaType.VERSION_1_7_0, ApiMediaType.VERSION_1_8_0,
80              ApiMediaType.VERSION_1_9_0, ApiMediaType.VERSION_1_10_0, ApiMediaType.VERSION_1_11_0 })
81  @RestService(
82      name = "externalapiworkflowdefinitions",
83      title = "External API Workflow Definitions Service",
84      notes = {},
85      abstractText = "Provides resources and operations related to the workflow definitions"
86  )
87  @Component(
88      immediate = true,
89      service = WorkflowDefinitionsEndpoint.class,
90      property = {
91          "service.description=External API - Workflow Definitions Endpoint",
92          "opencast.service.type=org.opencastproject.external.workflows.definitions",
93          "opencast.service.path=/api/workflow-definitions"
94      }
95  )
96  @JaxrsResource
97  public class WorkflowDefinitionsEndpoint {
98  
99    /**
100    * The logging facility
101    */
102   private static final Logger logger = LoggerFactory.getLogger(WorkflowDefinitionsEndpoint.class);
103 
104   /**
105    * The workflow service
106    */
107   private WorkflowService workflowService;
108 
109   /**
110    * OSGi DI
111    */
112   public WorkflowService getWorkflowService() {
113     return workflowService;
114   }
115 
116   /**
117    * OSGi DI
118    */
119   @Reference
120   public void setWorkflowService(WorkflowService workflowService) {
121     this.workflowService = workflowService;
122   }
123 
124   /**
125    * OSGi activation method
126    */
127   @Activate
128   void activate(ComponentContext cc) {
129     logger.info("Activating External API - Workflow Definitions Endpoint");
130   }
131 
132   @GET
133   @Path("/")
134   @RestQuery(
135       name = "getworkflowdefinitions",
136       description = "Returns a list of workflow definition.",
137       returnDescription = "",
138       restParameters = {
139           @RestParameter(name = "withoperations", description = "Whether the workflow operations should be included in "
140               + "the response", isRequired = false, type = BOOLEAN),
141           @RestParameter(name = "withconfigurationpanel", description = "Whether the workflow configuration panel "
142               + "should be included in the response", isRequired = false, type = BOOLEAN),
143           @RestParameter(name = "filter", description = "Usage [Filter Name]:[Value to Filter With]. Available filter: "
144               + "\"tag\"", isRequired = false, type = STRING),
145           @RestParameter(name = "sort", description = "Sort the results based upon a list of comma seperated sorting "
146               + "criteria. In the comma seperated list each type of sorting is specified as a pair such as: "
147               + "<Sort Name>:ASC or <Sort Name>:DESC. Adding the suffix ASC or DESC sets the order as ascending or "
148               + "descending order and is mandatory.", isRequired = false, type = STRING),
149           @RestParameter(name = "limit", description = "The maximum number of results to return for a single request.",
150               isRequired = false, type = INTEGER),
151           @RestParameter(name = "offset", description = "The index of the first result to return.",
152               isRequired = false, type = INTEGER) },
153       responses = {
154           @RestResponse(description = "A (potentially empty) list of workflow definitions is returned.",
155               responseCode = HttpServletResponse.SC_OK),
156           @RestResponse(description = "The request is invalid or inconsistent.",
157               responseCode = HttpServletResponse.SC_BAD_REQUEST)
158       })
159   public Response getWorkflowDefinitions(@HeaderParam("Accept") String acceptHeader,
160           @QueryParam("withoperations") boolean withOperations,
161           @QueryParam("withconfigurationpanel") boolean withConfigurationPanel,
162           @QueryParam("withconfigurationpaneljson") boolean withConfigurationPanelJson,
163           @QueryParam("filter") String filter,
164           @QueryParam("sort") String sort,
165           @QueryParam("offset") Integer offset,
166           @QueryParam("limit") Integer limit) {
167     Stream<WorkflowDefinition> workflowDefinitions;
168     try {
169       workflowDefinitions = workflowService.listAvailableWorkflowDefinitions().stream();
170     } catch (WorkflowDatabaseException e) {
171       logger.error("The workflow service was not able to get the workflow definitions:", e);
172       return ApiResponseBuilder.serverError("Could not retrieve workflow definitions, reason: '%s'", getMessage(e));
173     }
174 
175     // Apply filter
176     if (StringUtils.isNotBlank(filter)) {
177       for (String f : filter.split(",")) {
178         int sepIdx = f.indexOf(':');
179         if (sepIdx < 0 || sepIdx == f.length() - 1) {
180           logger.debug("No value for filter {} in filters list: {}", f, filter);
181           continue;
182         }
183         String name = f.substring(0, sepIdx);
184         String value = f.substring(sepIdx + 1);
185 
186         if ("tag".equals(name)) {
187           workflowDefinitions = workflowDefinitions.filter(wd -> ArrayUtils.contains(wd.getTags(), value));
188         }
189       }
190     }
191 
192     // Apply sort
193     // TODO: this seems to not function as intended
194     ComparatorChain<WorkflowDefinition> comparator = new ComparatorChain<>();
195     if (StringUtils.isNoneBlank(sort)) {
196       ArrayList<SortCriterion> sortCriteria = RestUtils.parseSortQueryParameter(sort);
197       for (SortCriterion criterion : sortCriteria) {
198         switch (criterion.getFieldName()) {
199           case "identifier":
200             comparator.addComparator((wd1, wd2) -> {
201               String s1 = defaultString(wd1.getId());
202               String s2 = defaultString(wd2.getId());
203               if (criterion.getOrder() == Descending) {
204                 return s2.compareTo(s1);
205               }
206               return s1.compareTo(s2);
207             });
208             break;
209           case "title":
210             comparator.addComparator((wd1, wd2) -> {
211               String s1 = defaultString(wd1.getTitle());
212               String s2 = defaultString(wd2.getTitle());
213               if (criterion.getOrder() == Descending) {
214                 return s2.compareTo(s1);
215               }
216               return s1.compareTo(s2);
217             });
218             break;
219           case "displayorder":
220             comparator.addComparator((wd1, wd2) -> {
221               if (criterion.getOrder() == Descending) {
222                 return Integer.compare(wd2.getDisplayOrder(), wd1.getDisplayOrder());
223               }
224               return Integer.compare(wd1.getDisplayOrder(), wd2.getDisplayOrder());
225             });
226             break;
227           default:
228             return RestUtil.R.badRequest(
229                     String.format("Unknown search criterion in request: %s", criterion.getFieldName()));
230         }
231       }
232     }
233     if (comparator.size() > 0) {
234       workflowDefinitions = workflowDefinitions.sorted(comparator);
235     }
236 
237     // Apply offset
238     if (offset != null && offset > 0) {
239       workflowDefinitions = workflowDefinitions.skip(offset);
240     }
241 
242     // Apply limit
243     if (limit != null && limit > 0) {
244       workflowDefinitions = workflowDefinitions.limit(limit);
245     }
246 
247     List<JsonObject> jsonObjects = workflowDefinitions
248         .map(wd -> workflowDefinitionToJSON(wd, withOperations, withConfigurationPanel, withConfigurationPanelJson))
249         .collect(Collectors.toList());
250 
251     JsonArray jsonArray = new JsonArray();
252     for (JsonObject obj : jsonObjects) {
253       jsonArray.add(obj);
254     }
255 
256     return ApiResponseBuilder.Json.ok(acceptHeader, jsonArray);
257   }
258 
259   @GET
260   @Path("{workflowDefinitionId}")
261   @RestQuery(
262       name = "getworkflowdefinition",
263       description = "Returns a single workflow definition.",
264       returnDescription = "",
265       pathParameters = {
266           @RestParameter(name = "workflowDefinitionId", description = "The workflow definition id", isRequired = true,
267               type = STRING)
268       },
269       restParameters = {
270           @RestParameter(name = "withoperations", description = "Whether the workflow operations should be included in "
271               + "the response", isRequired = false, type = BOOLEAN),
272           @RestParameter(name = "withconfigurationpaneljson", description = "Whether the workflow configuration panel "
273               + "in JSON should be included in the response", isRequired = false, type = BOOLEAN),
274           @RestParameter(name = "withconfigurationpanel", description = "Whether the workflow configuration panel "
275               + "should be included in the response", isRequired = false, type = BOOLEAN)
276       },
277       responses = {
278           @RestResponse(description = "The workflow definition is returned.",
279               responseCode = HttpServletResponse.SC_OK),
280           @RestResponse(description = "The specified workflow definition does not exist.",
281               responseCode = HttpServletResponse.SC_NOT_FOUND)
282       })
283   public Response getWorkflowDefinition(@HeaderParam("Accept") String acceptHeader,
284           @PathParam("workflowDefinitionId") String id, @QueryParam("withoperations") boolean withOperations,
285           @QueryParam("withconfigurationpanel") boolean withConfigurationPanel,
286           @QueryParam("withconfigurationpaneljson") boolean withConfigurationPanelJson) throws Exception {
287     WorkflowDefinition wd;
288     try {
289       wd = workflowService.getWorkflowDefinitionById(id);
290     } catch (NotFoundException e) {
291       return ApiResponseBuilder.notFound("Cannot find workflow definition with id '%s'.", id);
292     }
293 
294     return ApiResponseBuilder.Json.ok(acceptHeader, workflowDefinitionToJSON(wd, withOperations,
295         withConfigurationPanel, withConfigurationPanelJson));
296   }
297 
298   private JsonObject workflowDefinitionToJSON(WorkflowDefinition wd, boolean withOperations,
299       boolean withConfigurationPanel, boolean withConfigurationPanelJson) {
300     JsonObject json = new JsonObject();
301 
302     json.addProperty("identifier", wd.getId());
303     json.addProperty("title", safeString(wd.getTitle()));
304     json.addProperty("description", safeString(wd.getDescription()));
305     json.add("tags", arrayToJsonArray(wd.getTags()));
306     if (withConfigurationPanel) {
307       json.addProperty("configuration_panel", safeString(wd.getConfigurationPanel()));
308     }
309     if (withConfigurationPanelJson) {
310       json.addProperty("configuration_panel_json", safeString(wd.getConfigurationPanelJson()));
311     }
312     if (withOperations) {
313       JsonArray operationsArray = new JsonArray();
314       for (WorkflowOperationDefinition op : wd.getOperations()) {
315         operationsArray.add(workflowOperationDefinitionToJSON(op));
316       }
317       json.add("operations", operationsArray);
318     }
319 
320     return json;
321   }
322 
323   private JsonObject workflowOperationDefinitionToJSON(WorkflowOperationDefinition wod) {
324     JsonObject json = new JsonObject();
325 
326     json.addProperty("operation", wod.getId());
327     json.addProperty("description", safeString(wod.getDescription()));
328     JsonObject configJson = new JsonObject();
329     for (String key : wod.getConfigurationKeys()) {
330       String value = wod.getConfiguration(key);
331       configJson.addProperty(key, value);
332     }
333     json.add("configuration", configJson);
334     json.addProperty("if", safeString(wod.getExecutionCondition()));
335     json.addProperty("unless", safeString(wod.getSkipCondition()));
336     json.addProperty("fail_workflow_on_error", wod.isFailWorkflowOnException());
337     json.addProperty("error_handler_workflow", safeString(wod.getExceptionHandlingWorkflow()));
338     String retryStrategy = new RetryStrategy.Adapter().marshal(wod.getRetryStrategy());
339     json.addProperty("retry_strategy", safeString(retryStrategy));
340     json.addProperty("max_attempts", wod.getMaxAttempts());
341 
342     return json;
343   }
344 }