All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.opencastproject.external.endpoint.WorkflowsEndpoint Maven / Gradle / Ivy

The newest version!
/*
 * Licensed to The Apereo Foundation under one or more contributor license
 * agreements. See the NOTICE file distributed with this work for additional
 * information regarding copyright ownership.
 *
 *
 * The Apereo Foundation licenses this file to you under the Educational
 * Community License, Version 2.0 (the "License"); you may not use this file
 * except in compliance with the License. You may obtain a copy of the License
 * at:
 *
 *   http://opensource.org/licenses/ecl2.txt
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
 * License for the specific language governing permissions and limitations under
 * the License.
 *
 */
package org.opencastproject.external.endpoint;

import static com.entwinemedia.fn.data.json.Jsons.BLANK;
import static com.entwinemedia.fn.data.json.Jsons.ZERO;
import static com.entwinemedia.fn.data.json.Jsons.arr;
import static com.entwinemedia.fn.data.json.Jsons.f;
import static com.entwinemedia.fn.data.json.Jsons.obj;
import static com.entwinemedia.fn.data.json.Jsons.v;
import static java.time.ZoneOffset.UTC;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNoneBlank;
import static org.opencastproject.util.RestUtil.getEndpointUrl;
import static org.opencastproject.util.doc.rest.RestParameter.Type.BOOLEAN;
import static org.opencastproject.util.doc.rest.RestParameter.Type.INTEGER;
import static org.opencastproject.util.doc.rest.RestParameter.Type.STRING;

import org.opencastproject.elasticsearch.index.ElasticsearchIndex;
import org.opencastproject.elasticsearch.index.objects.event.Event;
import org.opencastproject.external.common.ApiMediaType;
import org.opencastproject.external.common.ApiResponses;
import org.opencastproject.external.common.ApiVersion;
import org.opencastproject.index.service.api.IndexService;
import org.opencastproject.mediapackage.MediaPackage;
import org.opencastproject.rest.RestConstants;
import org.opencastproject.security.api.UnauthorizedException;
import org.opencastproject.systems.OpencastConstants;
import org.opencastproject.util.NotFoundException;
import org.opencastproject.util.RestUtil;
import org.opencastproject.util.UrlSupport;
import org.opencastproject.util.data.Tuple;
import org.opencastproject.util.doc.rest.RestParameter;
import org.opencastproject.util.doc.rest.RestQuery;
import org.opencastproject.util.doc.rest.RestResponse;
import org.opencastproject.util.doc.rest.RestService;
import org.opencastproject.workflow.api.RetryStrategy;
import org.opencastproject.workflow.api.WorkflowDefinition;
import org.opencastproject.workflow.api.WorkflowInstance;
import org.opencastproject.workflow.api.WorkflowOperationInstance;
import org.opencastproject.workflow.api.WorkflowService;
import org.opencastproject.workflow.api.WorkflowStateException;

import com.entwinemedia.fn.data.Opt;
import com.entwinemedia.fn.data.json.Field;
import com.entwinemedia.fn.data.json.JValue;

import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;
import org.json.simple.parser.ParseException;
import org.osgi.service.component.ComponentContext;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.net.URI;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.DELETE;
import javax.ws.rs.FormParam;
import javax.ws.rs.GET;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Response;

@Path("/")
@Produces({ ApiMediaType.JSON, ApiMediaType.VERSION_1_1_0, ApiMediaType.VERSION_1_2_0, ApiMediaType.VERSION_1_3_0,
            ApiMediaType.VERSION_1_4_0, ApiMediaType.VERSION_1_5_0, ApiMediaType.VERSION_1_6_0,
            ApiMediaType.VERSION_1_7_0, ApiMediaType.VERSION_1_8_0, ApiMediaType.VERSION_1_9_0,
            ApiMediaType.VERSION_1_10_0, ApiMediaType.VERSION_1_11_0 })
@RestService(name = "externalapiworkflowinstances", title = "External API Workflow Instances Service", notes = {},
             abstractText = "Provides resources and operations related to the workflow instances")
@Component(
    immediate = true,
    service = WorkflowsEndpoint.class,
    property = {
        "service.description=External API - Workflow Instances Endpoint",
        "opencast.service.type=org.opencastproject.external.workflows.instances",
        "opencast.service.path=/api/workflows"
    }
)
public class WorkflowsEndpoint {
  /** The logging facility */
  private static final Logger logger = LoggerFactory.getLogger(WorkflowsEndpoint.class);

  /** Base URL of this endpoint */
  protected String endpointBaseUrl;

  /* OSGi service references */
  private WorkflowService workflowService;
  private ElasticsearchIndex elasticsearchIndex;
  private IndexService indexService;

  /** OSGi DI */
  @Reference
  public void setWorkflowService(WorkflowService workflowService) {
    this.workflowService = workflowService;
  }

  /** OSGi DI */
  @Reference
  public void setElasticsearchIndex(ElasticsearchIndex elasticsearchIndex) {
    this.elasticsearchIndex = elasticsearchIndex;
  }

  /** OSGi DI */
  @Reference
  public void setIndexService(IndexService indexService) {
    this.indexService = indexService;
  }

  /**
   * OSGi activation method
   */
  @Activate
  void activate(ComponentContext cc) {
    logger.info("Activating External API - Workflow Instances Endpoint");

    final Tuple endpointUrl = getEndpointUrl(cc, OpencastConstants.EXTERNAL_API_URL_ORG_PROPERTY,
            RestConstants.SERVICE_PATH_PROPERTY);
    endpointBaseUrl = UrlSupport.concat(endpointUrl.getA(), endpointUrl.getB());
    logger.debug("Configured service endpoint is {}", endpointBaseUrl);
  }

  @POST
  @Path("")
  @RestQuery(name = "createworkflowinstance", description = "Creates a workflow instance.", returnDescription = "", restParameters = {
          @RestParameter(name = "event_identifier", description = "The event identifier this workflow should run against", isRequired = true, type = STRING),
          @RestParameter(name = "workflow_definition_identifier", description = "The identifier of the workflow definition to use", isRequired = true, type = STRING),
          @RestParameter(name = "configuration", description = "The optional configuration for this workflow", isRequired = false, type = STRING),
          @RestParameter(name = "withoperations", description = "Whether the workflow operations should be included in the response", isRequired = false, type = BOOLEAN),
          @RestParameter(name = "withconfiguration", description = "Whether the workflow configuration should be included in the response", isRequired = false, type = BOOLEAN), }, responses = {
          @RestResponse(description = "A new workflow is created and its identifier is returned in the Location header.", responseCode = HttpServletResponse.SC_CREATED),
          @RestResponse(description = "The request is invalid or inconsistent.", responseCode = HttpServletResponse.SC_BAD_REQUEST),
          @RestResponse(description = "The event or workflow definition could not be found.", responseCode = HttpServletResponse.SC_NOT_FOUND) })
  public Response createWorkflowInstance(@HeaderParam("Accept") String acceptHeader,
          @FormParam("event_identifier") String eventId,
          @FormParam("workflow_definition_identifier") String workflowDefinitionIdentifier,
          @FormParam("configuration") String configuration, @QueryParam("withoperations") boolean withOperations,
          @QueryParam("withconfiguration") boolean withConfiguration) {
    if (isBlank(eventId)) {
      return RestUtil.R.badRequest("Required parameter 'event_identifier' is missing or invalid");
    }

    if (isBlank(workflowDefinitionIdentifier)) {
      return RestUtil.R.badRequest("Required parameter 'workflow_definition_identifier' is missing or invalid");
    }

    try {
      // Media Package
      Opt event = indexService.getEvent(eventId, elasticsearchIndex);
      if (event.isNone()) {
        return ApiResponses.notFound("Cannot find an event with id '%s'.", eventId);
      }
      MediaPackage mp = indexService.getEventMediapackage(event.get());

      // Workflow definition
      WorkflowDefinition wd;
      try {
        wd = workflowService.getWorkflowDefinitionById(workflowDefinitionIdentifier);
      } catch (NotFoundException e) {
        return ApiResponses.notFound("Cannot find a workflow definition with id '%s'.", workflowDefinitionIdentifier);
      }

      // Configuration
      Map properties = new HashMap<>();
      if (isNoneBlank(configuration)) {
        JSONParser parser = new JSONParser();
        try {
          properties.putAll((JSONObject) parser.parse(configuration));
        } catch (ParseException e) {
          return RestUtil.R.badRequest("Passed parameter 'configuration' is invalid JSON.");
        }
      }

      // Start workflow
      WorkflowInstance wi = workflowService.start(wd, mp, null, properties);
      return ApiResponses.Json.created(acceptHeader, URI.create(getWorkflowUrl(wi.getId())),
              workflowInstanceToJSON(wi, withOperations, withConfiguration));
    } catch (IllegalStateException e) {
      final ApiVersion requestedVersion = ApiMediaType.parse(acceptHeader).getVersion();
      return ApiResponses.Json.conflict(requestedVersion, obj(f("message", v(e.getMessage(), BLANK))));
    } catch (Exception e) {
      logger.error("Could not create workflow instances", e);
      return ApiResponses.serverError("Could not create workflow instances, reason: '%s'", e.getMessage());
    }
  }

  @GET
  @Path("{workflowInstanceId}")
  @RestQuery(name = "getworkflowinstance", description = "Returns a single workflow instance.", returnDescription = "", pathParameters = {
          @RestParameter(name = "workflowInstanceId", description = "The workflow instance id", isRequired = true, type = INTEGER) }, restParameters = {
          @RestParameter(name = "withoperations", description = "Whether the workflow operations should be included in the response", isRequired = false, type = BOOLEAN),
          @RestParameter(name = "withconfiguration", description = "Whether the workflow configuration should be included in the response", isRequired = false, type = BOOLEAN) }, responses = {
          @RestResponse(description = "The workflow instance is returned.", responseCode = HttpServletResponse.SC_OK),
          @RestResponse(description = "The user doesn't have the rights to make this request.", responseCode = HttpServletResponse.SC_FORBIDDEN),
          @RestResponse(description = "The specified workflow instance does not exist.", responseCode = HttpServletResponse.SC_NOT_FOUND) })
  public Response getWorkflowInstance(@HeaderParam("Accept") String acceptHeader,
          @PathParam("workflowInstanceId") Long id, @QueryParam("withoperations") boolean withOperations,
          @QueryParam("withconfiguration") boolean withConfiguration) {
    WorkflowInstance wi;
    try {
      wi = workflowService.getWorkflowById(id);
    } catch (NotFoundException e) {
      return ApiResponses.notFound("Cannot find workflow instance with id '%d'.", id);
    } catch (UnauthorizedException e) {
      return Response.status(Response.Status.FORBIDDEN).build();
    } catch (Exception e) {
      logger.error("The workflow service was not able to get the workflow instance", e);
      return ApiResponses.serverError("Could not retrieve workflow instance, reason: '%s'", e.getMessage());
    }

    return ApiResponses.Json.ok(acceptHeader, workflowInstanceToJSON(wi, withOperations, withConfiguration));
  }

  @PUT
  @Path("{workflowInstanceId}")
  @RestQuery(name = "updateworkflowinstance", description = "Creates a workflow instance.", returnDescription = "", pathParameters = {
          @RestParameter(name = "workflowInstanceId", description = "The workflow instance id", isRequired = true, type = INTEGER) }, restParameters = {
          @RestParameter(name = "configuration", description = "The optional configuration for this workflow", isRequired = false, type = STRING),
          @RestParameter(name = "state", description = "The optional state transition for this workflow", isRequired = false, type = STRING),
          @RestParameter(name = "withoperations", description = "Whether the workflow operations should be included in the response", isRequired = false, type = BOOLEAN),
          @RestParameter(name = "withconfiguration", description = "Whether the workflow configuration should be included in the response", isRequired = false, type = BOOLEAN), }, responses = {
          @RestResponse(description = "The workflow instance is updated.", responseCode = HttpServletResponse.SC_OK),
          @RestResponse(description = "The request is invalid or inconsistent.", responseCode = HttpServletResponse.SC_BAD_REQUEST),
          @RestResponse(description = "The user doesn't have the rights to make this request.", responseCode = HttpServletResponse.SC_FORBIDDEN),
          @RestResponse(description = "The workflow instance could not be found.", responseCode = HttpServletResponse.SC_NOT_FOUND),
          @RestResponse(description = "The workflow instance cannot transition to this state.", responseCode = HttpServletResponse.SC_CONFLICT) })
  public Response updateWorkflowInstance(@HeaderParam("Accept") String acceptHeader,
          @PathParam("workflowInstanceId") Long id, @FormParam("configuration") String configuration,
          @FormParam("state") String stateStr, @QueryParam("withoperations") boolean withOperations,
          @QueryParam("withconfiguration") boolean withConfiguration) {
    try {
      boolean changed = false;
      WorkflowInstance wi = workflowService.getWorkflowById(id);

      // Configuration
      if (isNoneBlank(configuration)) {
        JSONParser parser = new JSONParser();
        try {
          Map properties = new HashMap<>((JSONObject) parser.parse(configuration));

          // Remove old configuration
          wi.getConfigurationKeys().forEach(wi::removeConfiguration);
          // Add new configuration
          properties.forEach(wi::setConfiguration);

          changed = true;
        } catch (ParseException e) {
          return RestUtil.R.badRequest("Passed parameter 'configuration' is invalid JSON.");
        }
      }

      // TODO: does it make sense to change the media package?

      if (changed) {
        workflowService.update(wi);
      }

      // State change
      if (isNoneBlank(stateStr)) {
        WorkflowInstance.WorkflowState state;
        try {
          state = jsonToEnum(WorkflowInstance.WorkflowState.class, stateStr);
        } catch (IllegalArgumentException e) {
          return RestUtil.R.badRequest(String.format("Invalid workflow state '%s'", stateStr));
        }

        WorkflowInstance.WorkflowState currentState = wi.getState();
        if (state != currentState) {
          // Allowed transitions:
          //
          //   instantiated -> paused, stopped, running
          //   running      -> paused, stopped
          //   failing      -> paused, stopped
          //   paused       -> paused, stopped, running
          //   succeeded    -> paused, stopped
          //   stopped      -> paused, stopped
          //   failed       -> paused, stopped
          switch (state) {
            case PAUSED:
              workflowService.suspend(wi.getId());
              break;
            case STOPPED:
              workflowService.stop(wi.getId());
              break;
            case RUNNING:
              if (currentState == WorkflowInstance.WorkflowState.INSTANTIATED
                      || currentState == WorkflowInstance.WorkflowState.PAUSED) {
                workflowService.resume(wi.getId());
              } else {
                return RestUtil.R.conflict(
                        String.format("Cannot resume from workflow state '%s'", currentState.toString().toLowerCase()));
              }
              break;
            default:
              return RestUtil.R.conflict(
                      String.format("Cannot transition state from '%s' to '%s'", currentState.toString().toLowerCase(),
                              stateStr));
          }
        }
      }

      wi = workflowService.getWorkflowById(id);
      return ApiResponses.Json.ok(acceptHeader, workflowInstanceToJSON(wi, withOperations, withConfiguration));
    } catch (NotFoundException e) {
      return ApiResponses.notFound("Cannot find workflow instance with id '%d'.", id);
    } catch (UnauthorizedException e) {
      return Response.status(Response.Status.FORBIDDEN).build();
    } catch (Exception e) {
      logger.error("The workflow service was not able to get the workflow instance", e);
      return ApiResponses.serverError("Could not retrieve workflow instance, reason: '%s'", e.getMessage());
    }
  }

  @DELETE
  @Path("{workflowInstanceId}")
  @RestQuery(name = "deleteworkflowinstance", description = "Deletes a workflow instance.", returnDescription = "", pathParameters = {
          @RestParameter(name = "workflowInstanceId", description = "The workflow instance id", isRequired = true, type = INTEGER) }, responses = {
          @RestResponse(description = "The workflow instance has been deleted.", responseCode = HttpServletResponse.SC_NO_CONTENT),
          @RestResponse(description = "The user doesn't have the rights to make this request.", responseCode = HttpServletResponse.SC_FORBIDDEN),
          @RestResponse(description = "The specified workflow instance does not exist.", responseCode = HttpServletResponse.SC_NOT_FOUND),
          @RestResponse(description = "The workflow instance cannot be deleted in this state.", responseCode = HttpServletResponse.SC_CONFLICT) })
  public Response deleteWorkflowInstance(@HeaderParam("Accept") String acceptHeader,
          @PathParam("workflowInstanceId") Long id) {
    try {
      workflowService.remove(id);
    } catch (WorkflowStateException e) {
      return RestUtil.R.conflict("Cannot delete workflow instance in this workflow state");
    } catch (NotFoundException e) {
      return ApiResponses.notFound("Cannot find workflow instance with id '%d'.", id);
    } catch (UnauthorizedException e) {
      return Response.status(Response.Status.FORBIDDEN).build();
    } catch (Exception e) {
      logger.error("Could not delete workflow instances", e);
      return ApiResponses.serverError("Could not delete workflow instances, reason: '%s'", e.getMessage());
    }

    return Response.noContent().build();
  }

  private JValue workflowInstanceToJSON(WorkflowInstance wi, boolean withOperations, boolean withConfiguration) {
    List fields = new ArrayList<>();

    fields.add(f("identifier", v(wi.getId())));
    fields.add(f("title", v(wi.getTitle(), BLANK)));
    fields.add(f("description", v(wi.getDescription(), BLANK)));
    fields.add(f("workflow_definition_identifier", v(wi.getTemplate(), BLANK)));
    fields.add(f("event_identifier", v(wi.getMediaPackage().getIdentifier().toString())));
    fields.add(f("creator", v(wi.getCreatorName())));
    fields.add(f("state", enumToJSON(wi.getState())));
    if (withOperations) {
      fields.add(f("operations", arr(wi.getOperations()
                                       .stream()
                                       .map(this::workflowOperationInstanceToJSON)
                                       .collect(Collectors.toList()))));
    }
    if (withConfiguration) {
      fields.add(f("configuration", obj(wi.getConfigurationKeys()
                                          .stream()
                                          .map(key -> f(key, wi.getConfiguration(key)))
                                          .collect(Collectors.toList()))));
    }

    return obj(fields);
  }

  private JValue workflowOperationInstanceToJSON(WorkflowOperationInstance woi) {
    List fields = new ArrayList<>();
    DateTimeFormatter dateFormatter = DateTimeFormatter.ISO_DATE_TIME;

    // The job ID can be null if the workflow was just created
    fields.add(f("identifier", v(woi.getId(), BLANK)));
    fields.add(f("operation", v(woi.getTemplate())));
    fields.add(f("description", v(woi.getDescription(), BLANK)));
    fields.add(f("state", enumToJSON(woi.getState())));
    fields.add(f("time_in_queue", v(woi.getTimeInQueue(), ZERO)));
    fields.add(f("host", v(woi.getExecutionHost(), BLANK)));
    fields.add(f("if", v(woi.getExecutionCondition(), BLANK)));
    fields.add(f("fail_workflow_on_error", v(woi.isFailOnError())));
    fields.add(f("error_handler_workflow", v(woi.getExceptionHandlingWorkflow(), BLANK)));
    fields.add(f("retry_strategy", v(new RetryStrategy.Adapter().marshal(woi.getRetryStrategy()), BLANK)));
    fields.add(f("max_attempts", v(woi.getMaxAttempts())));
    fields.add(f("failed_attempts", v(woi.getFailedAttempts())));
    fields.add(f("configuration", obj(woi.getConfigurationKeys()
                                         .stream()
                                         .map(key -> f(key, woi.getConfiguration(key)))
                                         .collect(Collectors.toList()))));
    if (woi.getDateStarted() != null) {
      fields.add(f("start", v(dateFormatter.format(woi.getDateStarted().toInstant().atZone(UTC)))));
    } else {
      fields.add(f("start", BLANK));
    }
    if (woi.getDateCompleted() != null) {
      fields.add(f("completion", v(dateFormatter.format(woi.getDateCompleted().toInstant().atZone(UTC)))));
    } else {
      fields.add(f("completion", BLANK));
    }

    return obj(fields);
  }

  private JValue enumToJSON(Enum e) {
    return e == null ? null : v(e.toString().toLowerCase());
  }

  private > T jsonToEnum(Class enumType, String name) {
    return Enum.valueOf(enumType, name.toUpperCase());
  }

  private String getWorkflowUrl(long workflowInstanceId) {
    return UrlSupport.concat(endpointBaseUrl, Long.toString(workflowInstanceId));
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy