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