Skip to content

Commit

Permalink
feat(api): add PUT /api/v1/apis/generator
Browse files Browse the repository at this point in the history
When creating an integration from the OpenAPI document prior to saving
the integration there is a need to modify the OpenAPI document and have
the changes reflect in the integration.

This adds a `PUT /api/v1/apis/generator` that returns HTTP status `202`
and the updated integration when the provided OpenAPI document produces
changes to the integration; and HTTP status `304` with no content when
there are no changes to the integration.

There's a side effect of storing the updated OpenAPI document and
referencing it from the integration resources, if there was an update.

Fixes syndesisio#5719
  • Loading branch information
zregvart committed Jun 13, 2019
1 parent 0fb6377 commit 6554a0a
Show file tree
Hide file tree
Showing 6 changed files with 413 additions and 290 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,19 @@
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

import io.syndesis.common.model.DataShape;
import io.syndesis.common.model.Kind;
import io.syndesis.common.model.ResourceIdentifier;
import io.syndesis.common.model.connection.Connection;
import io.syndesis.common.model.connection.Connector;
import io.syndesis.common.model.integration.Flow;
import io.syndesis.common.model.integration.Integration;
import io.syndesis.common.model.integration.Step;
import io.syndesis.common.model.openapi.OpenApi;
import io.syndesis.common.util.SyndesisServerException;
import io.syndesis.integration.api.IntegrationResourceManager;
import io.syndesis.server.api.generator.APIGenerator;
import io.syndesis.server.api.generator.APIIntegration;
import io.syndesis.server.api.generator.ProvidedApiTemplate;
Expand Down Expand Up @@ -56,6 +63,15 @@ public static APIIntegration generateIntegrationFrom(final APIFormData apiFormDa
return apiGenerator.generateIntegration(spec, template);
}

public static Optional<OpenApi> specificationFrom(IntegrationResourceManager resourceManager, final Integration integration) {
final Optional<ResourceIdentifier> specification = integration.getResources().stream().filter(r -> r.getKind() == Kind.OpenApi)
.max(ResourceIdentifier.LATEST_VERSION);

return specification
.flatMap(s -> s.getId()
.flatMap(resourceManager::loadOpenApiDefinition));
}

public static APIIntegration generateIntegrationUpdateFrom(final Integration existing, final APIFormData apiFormData, final DataManager dataManager,
final APIGenerator apiGenerator) {
final APIIntegration newApiIntegration = generateIntegrationFrom(apiFormData, dataManager, apiGenerator);
Expand All @@ -68,6 +84,67 @@ public static APIIntegration generateIntegrationUpdateFrom(final Integration exi
return new APIIntegration(updatedIntegration, newApiIntegration.getSpec());
}

public static Integration updateFlowsAndStartAndEndDataShapes(final Integration existing, final Integration given) {
// will contain updated flows
final List<Flow> updatedFlows = new ArrayList<>(given.getFlows().size());

for (final Flow givenFlow : given.getFlows()) {
final String flowId = givenFlow.getId().get();

final Optional<Flow> maybeExistingFlow = existing.findFlowById(flowId);
if (!maybeExistingFlow.isPresent()) {
// this is a flow generated from a new operation or it
// has it's operation id changed, either way we only
// need to add it, since we don't know what flow we need
// to update
updatedFlows.add(givenFlow);
continue;
}

final List<Step> givenSteps = givenFlow.getSteps();
if (givenSteps.size() != 2) {
throw new IllegalArgumentException("Expecting to get exactly two steps per flow");
}

// this is a freshly minted flow from the specification
// there should be only two steps (start and end) in the
// flow
final Step givenStart = givenSteps.get(0);
final Optional<DataShape> givenStartDataShape = givenStart.outputDataShape();

// generated flow has only a start and an end, start is at 0
// and the end is at 1
final Step givenEnd = givenSteps.get(1);
final Optional<DataShape> givenEndDataShape = givenEnd.inputDataShape();

final Flow existingFlow = maybeExistingFlow.get();
final List<Step> existingSteps = existingFlow.getSteps();

// readability
final int start = 0;
final int end = existingSteps.size() - 1;

// now we update the data shapes of the start and end steps
final Step existingStart = existingSteps.get(start);
final Step updatedStart = existingStart.updateOutputDataShape(givenStartDataShape);

final Step existingEnd = existingSteps.get(end);
final Step updatedEnd = existingEnd.updateInputDataShape(givenEndDataShape);

final List<Step> updatedSteps = new ArrayList<>(existingSteps);
updatedSteps.set(start, updatedStart);
updatedSteps.set(end, updatedEnd);

final Flow updatedFlow = existingFlow.builder().name(givenFlow.getName()).description(givenFlow.getDescription()).steps(updatedSteps).build();
updatedFlows.add(updatedFlow);
}

return existing.builder().flows(updatedFlows)
// we replace all resources counting that the only resource
// present is the OpenAPI specification
.resources(given.getResources()).build();
}

static String getSpec(final APIFormData apiFormData) {
try (BufferedSource source = Okio.buffer(Okio.source(apiFormData.getSpecification()))) {
return source.readUtf8();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,36 +15,72 @@
*/
package io.syndesis.server.endpoint.v1.handler.api;

import java.io.InputStream;
import java.util.Objects;

import javax.ws.rs.Consumes;
import javax.ws.rs.FormParam;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;

import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.syndesis.common.model.api.APISummary;
import io.syndesis.common.model.integration.Integration;
import io.syndesis.common.model.openapi.OpenApi;
import io.syndesis.integration.api.IntegrationResourceManager;
import io.syndesis.server.api.generator.APIGenerator;
import io.syndesis.server.api.generator.APIIntegration;
import io.syndesis.server.api.generator.APIValidationContext;
import io.syndesis.server.dao.manager.DataManager;
import io.syndesis.server.endpoint.v1.handler.BaseHandler;

import org.jboss.resteasy.annotations.providers.multipart.MultipartForm;
import org.springframework.stereotype.Component;

import javax.ws.rs.Consumes;
import javax.ws.rs.FormParam;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import java.io.InputStream;

@Path("/apis")
@Api(value = "apis")
@Component
public class ApiHandler extends BaseHandler {

private final APIGenerator apiGenerator;

public ApiHandler(final DataManager dataMgr, APIGenerator apiGenerator) {
private final IntegrationResourceManager resourceManager;

public static class APIFormData {

@FormParam("specification")
private InputStream specification;

public InputStream getSpecification() {
return specification;
}

public void setSpecification(final InputStream specification) {
this.specification = specification;
}
}

public static class APIUpdateFormData extends APIFormData {
@FormParam("integration")
private Integration integration;

public Integration getIntegration() {
return integration;
}

public void setIntegration(final Integration integration) {
this.integration = integration;
}
}

public ApiHandler(final DataManager dataMgr, final IntegrationResourceManager resourceManager, final APIGenerator apiGenerator) {
super(dataMgr);
this.resourceManager = resourceManager;
this.apiGenerator = apiGenerator;
}

Expand Down Expand Up @@ -72,22 +108,38 @@ public Integration createIntegrationFromAPI(@MultipartForm final APIFormData api
@Consumes(MediaType.MULTIPART_FORM_DATA)
@ApiOperation("Validates the API and provides a summary of the operations")
public APISummary info(@MultipartForm final APIFormData apiFormData) {
String spec = ApiGeneratorHelper.getSpec(apiFormData);
final String spec = ApiGeneratorHelper.getSpec(apiFormData);
return apiGenerator.info(spec, APIValidationContext.PROVIDED_API);
}

public static class APIFormData {
@PUT
@Path("/generator")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.MULTIPART_FORM_DATA)
@ApiOperation("Update the provided integration from a API specification. Does not store it in the database")
public Response updateIntegrationFromSpecification(@MultipartForm final APIUpdateFormData apiUpdateFormData) {
final DataManager dataManager = getDataManager();
final Integration existing = apiUpdateFormData.integration;

@FormParam("specification")
private InputStream specification;
final APIIntegration apiIntegration = ApiGeneratorHelper.generateIntegrationUpdateFrom(existing, apiUpdateFormData, dataManager, apiGenerator);

public void setSpecification(InputStream specification) {
this.specification = specification;
}
final Integration givenIntegration = apiIntegration.getIntegration();

public InputStream getSpecification() {
return specification;
final Integration updated = ApiGeneratorHelper.updateFlowsAndStartAndEndDataShapes(existing, givenIntegration);

final OpenApi existingApiSpecification = ApiGeneratorHelper.specificationFrom(resourceManager, existing).orElse(null);

if (Objects.equals(existing.getFlows(), updated.getFlows()) && Objects.equals(existingApiSpecification, apiIntegration.getSpec())) {
// no changes were made to the flows or to the specification
return Response.notModified().build();
}
}

// store the OpenAPI resource, we keep the old one as it might
// be referenced from Integration's stored in IntegrationDeployent's
// this gives us a rollback mechanism
final OpenApi openApi = apiIntegration.getSpec();
dataManager.store(openApi, OpenApi.class);

return Response.accepted(updated).build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,10 @@
*/
package io.syndesis.server.endpoint.v1.handler.integration;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;

import javax.validation.constraints.NotNull;
import javax.ws.rs.Consumes;
Expand All @@ -42,12 +39,7 @@
import io.swagger.annotations.ApiResponse;
import io.swagger.annotations.ApiResponses;
import io.swagger.annotations.ResponseHeader;
import io.syndesis.common.model.DataShape;
import io.syndesis.common.model.Kind;
import io.syndesis.common.model.ResourceIdentifier;
import io.syndesis.common.model.integration.Flow;
import io.syndesis.common.model.integration.Integration;
import io.syndesis.common.model.integration.Step;
import io.syndesis.common.model.openapi.OpenApi;
import io.syndesis.integration.api.IntegrationResourceManager;
import io.syndesis.server.api.generator.APIGenerator;
Expand Down Expand Up @@ -112,7 +104,7 @@ public IntegrationSpecificationHandler(final IntegrationHandler integrationHandl
fetch(@NotNull @PathParam("id") @ApiParam(required = true, example = "integration-id", value = "The ID of the integration") final String id) {
final Integration integration = integrationHandler.getIntegration(id);

return specificationFrom(integration)
return ApiGeneratorHelper.specificationFrom(resourceManager, integration)
.map(IntegrationSpecificationHandler::createResponseFrom)
.orElse(NOT_FOUND);
}
Expand All @@ -130,9 +122,9 @@ public void update(@NotNull @PathParam("id") @ApiParam(required = true, example

final Integration givenIntegration = apiIntegration.getIntegration();

final Integration updated = updateFlowsAndStartAndEndDataShapes(existing, givenIntegration);
final Integration updated = ApiGeneratorHelper.updateFlowsAndStartAndEndDataShapes(existing, givenIntegration);

final OpenApi existingApiSpecification = specificationFrom(existing).orElse(null);
final OpenApi existingApiSpecification = ApiGeneratorHelper.specificationFrom(resourceManager, existing).orElse(null);

if (Objects.equals(existing.getFlows(), updated.getFlows()) && Objects.equals(existingApiSpecification, apiIntegration.getSpec())) {
// no changes were made to the flows or to the specification
Expand All @@ -148,15 +140,6 @@ public void update(@NotNull @PathParam("id") @ApiParam(required = true, example
integrationHandler.update(id, updated);
}

Optional<OpenApi> specificationFrom(final Integration integration) {
final Optional<ResourceIdentifier> specification = integration.getResources().stream().filter(r -> r.getKind() == Kind.OpenApi)
.max(ResourceIdentifier.LATEST_VERSION);

return specification
.flatMap(s -> s.getId()
.flatMap(resourceManager::loadOpenApiDefinition));
}

static Response createResponseFrom(final OpenApi document) {
final byte[] bytes = document.getDocument();
if (bytes == null || bytes.length == 0) {
Expand All @@ -171,64 +154,4 @@ static Response createResponseFrom(final OpenApi document) {
.build();
}

static Integration updateFlowsAndStartAndEndDataShapes(final Integration existing, final Integration given) {
// will contain updated flows
final List<Flow> updatedFlows = new ArrayList<>(given.getFlows().size());

for (final Flow givenFlow : given.getFlows()) {
final String flowId = givenFlow.getId().get();

final Optional<Flow> maybeExistingFlow = existing.findFlowById(flowId);
if (!maybeExistingFlow.isPresent()) {
// this is a flow generated from a new operation or it
// has it's operation id changed, either way we only
// need to add it, since we don't know what flow we need
// to update
updatedFlows.add(givenFlow);
continue;
}

final List<Step> givenSteps = givenFlow.getSteps();
if (givenSteps.size() != 2) {
throw new IllegalArgumentException("Expecting to get exactly two steps per flow");
}

// this is a freshly minted flow from the specification
// there should be only two steps (start and end) in the
// flow
final Step givenStart = givenSteps.get(0);
final Optional<DataShape> givenStartDataShape = givenStart.outputDataShape();

// generated flow has only a start and an end, start is at 0
// and the end is at 1
final Step givenEnd = givenSteps.get(1);
final Optional<DataShape> givenEndDataShape = givenEnd.inputDataShape();

final Flow existingFlow = maybeExistingFlow.get();
final List<Step> existingSteps = existingFlow.getSteps();

// readability
final int start = 0;
final int end = existingSteps.size() - 1;

// now we update the data shapes of the start and end steps
final Step existingStart = existingSteps.get(start);
final Step updatedStart = existingStart.updateOutputDataShape(givenStartDataShape);

final Step existingEnd = existingSteps.get(end);
final Step updatedEnd = existingEnd.updateInputDataShape(givenEndDataShape);

final List<Step> updatedSteps = new ArrayList<>(existingSteps);
updatedSteps.set(start, updatedStart);
updatedSteps.set(end, updatedEnd);

final Flow updatedFlow = existingFlow.builder().name(givenFlow.getName()).description(givenFlow.getDescription()).steps(updatedSteps).build();
updatedFlows.add(updatedFlow);
}

return existing.builder().flows(updatedFlows)
// we replace all resources counting that the only resource
// present is the OpenAPI specification
.resources(given.getResources()).build();
}
}
Loading

0 comments on commit 6554a0a

Please sign in to comment.