Skip to content

Commit

Permalink
feat: Custom connector REST API
Browse files Browse the repository at this point in the history
Extends the contract of `ConnectorGenerator` to provide a basic
information about the new custom connector.

Adds REST API for Custom connectors, first set of API endpoints deals
with enumerating connector templates:

 - `GET /api/v1/custom/connectors` - lists all known connector templates
 - `GET /api/v1/custom/connectors/{id}` - fetches specific connector
   template
 - `POST /api/v1/custom/connectors/{id}/info` - receives a custom
   connector definition, combines it with the template and using a
   connector generator provides basic information about the connector
   (used for confirmation/summary step before creating a new custom
   connector)
 - `POST /api/v1/custom/connectors/{id}/validation` - validates the
   given custom connector definition
 - `POST /api/v1/custom/connectors/{id}` - creates a new custom
   connector

Fixes syndesisio#434
  • Loading branch information
zregvart committed Nov 25, 2017
1 parent 0552c23 commit 6a2d502
Show file tree
Hide file tree
Showing 14 changed files with 529 additions and 87 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -23,29 +23,33 @@
import io.syndesis.core.Names;
import io.syndesis.model.connection.Connector;
import io.syndesis.model.connection.ConnectorTemplate;
import io.syndesis.model.connection.CustomConnector;

public interface ConnectorGenerator {

default Connector baseConnectorFrom(final ConnectorTemplate connectorTemplate, final Connector template) {
default Connector baseConnectorFrom(final ConnectorTemplate connectorTemplate,
final CustomConnector customConnector) {
final Set<String> properties = connectorTemplate.getProperties().keySet();

final Map<String, String> configuredProperties = template.getConfiguredProperties()//
final Map<String, String> configuredProperties = customConnector.getConfiguredProperties()//
.entrySet().stream()//
.filter(e -> properties.contains(e.getKey()))//
.collect(Collectors.toMap(Entry::getKey, Entry::getValue));

final Connector.Builder connectorBuilder = new Connector.Builder()//
.id(template.getId()
.orElseGet(() -> Names.sanitize(connectorTemplate.getName()) + ":" + Names.sanitize(template.getName())))//
.name(template.getName())//
.description(template.getDescription())//
.icon(template.getIcon())//
.id(customConnector.getId().orElseGet(
() -> Names.sanitize(connectorTemplate.getName()) + ":" + Names.sanitize(customConnector.getName())))//
.name(customConnector.getName())//
.description(customConnector.getDescription())//
.icon(customConnector.getIcon())//
.properties(connectorTemplate.getConnectorProperties())//
.configuredProperties(configuredProperties)//
.connectorGroup(connectorTemplate.getConnectorGroup());

return connectorBuilder.build();
}

Connector generate(ConnectorTemplate connectorTemplate, Connector template);
Connector generate(ConnectorTemplate connectorTemplate, CustomConnector customConnector);

Connector info(ConnectorTemplate connectorTemplate, CustomConnector customConnector);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/**
* Copyright (C) 2016 Red Hat, Inc.
*
* Licensed under the Apache 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://www.apache.org/licenses/LICENSE-2.0
*
* 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 io.syndesis.connector.generator.swagger;

import java.util.Arrays;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;

import io.swagger.models.Swagger;

enum DefaultPropertyValues {

host {
@Override
protected Function<Swagger, String> propertyValueExtractor() {
return Swagger::getHost;
}
};

private static final Set<String> KNOWN_PROPERTIES = Arrays.stream(values()).map(DefaultPropertyValues::name)
.collect(Collectors.toSet());

protected abstract Function<Swagger, String> propertyValueExtractor();

static Optional<String> fetchValue(final String propertyName, final Swagger swagger) {
if (!KNOWN_PROPERTIES.contains(propertyName)) {
return Optional.empty();
}

final DefaultPropertyValues defaultPropertyValue = DefaultPropertyValues.valueOf(propertyName);

final String value = defaultPropertyValue.propertyValueExtractor().apply(swagger);

return Optional.ofNullable(value);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
import io.syndesis.model.connection.ConfigurationProperty.PropertyValue;
import io.syndesis.model.connection.Connector;
import io.syndesis.model.connection.ConnectorTemplate;
import io.syndesis.model.connection.CustomConnector;

import static io.syndesis.connector.generator.swagger.DataShapeHelper.createShapeFromModel;
import static io.syndesis.connector.generator.swagger.DataShapeHelper.createShapeFromResponse;
Expand Down Expand Up @@ -76,24 +77,29 @@ public class SwaggerConnectorGenerator implements ConnectorGenerator {
}

@Override
public Connector generate(final ConnectorTemplate connectorTemplate, final Connector template) {
final Map<String, String> configuredProperties = template.getConfiguredProperties();
public Connector generate(final ConnectorTemplate connectorTemplate, final CustomConnector customConnector) {
final Connector connector = basicConnector(connectorTemplate, customConnector);

final String specification = configuredProperties.get("specification");
final String specification = requiredSpecification(customConnector);

if (specification == null) {
throw new IllegalStateException(
"Configured properties of the given Connector template does not include `specification` property");
}
return configureConnector(connectorTemplate, connector, specification);
}

@Override
public Connector info(final ConnectorTemplate connectorTemplate, final CustomConnector customConnector) {
return basicConnector(connectorTemplate, customConnector);
}

/* default */ Connector basicConnector(final ConnectorTemplate connectorTemplate,
final CustomConnector customConnector) {
final String specification = requiredSpecification(customConnector);

final Connector baseConnector = baseConnectorFrom(connectorTemplate, template);
final Connector baseConnector = baseConnectorFrom(connectorTemplate, customConnector);

final Connector connector = new Connector.Builder()//
return new Connector.Builder()//
.createFrom(baseConnector)//
.putConfiguredProperty("specification", specification)//
.build();

return configureConnector(connectorTemplate, connector, specification);
}

/* default */ static void addGlobalParameters(final Connector.Builder builder, final Swagger swagger) {
Expand Down Expand Up @@ -290,6 +296,18 @@ public Connector generate(final ConnectorTemplate connectorTemplate, final Conne
return property.build();
}

/* default */ static String requiredSpecification(final CustomConnector customConnector) {
final Map<String, String> configuredProperties = customConnector.getConfiguredProperties();

final String specification = configuredProperties.get("specification");

if (specification == null) {
throw new IllegalStateException(
"Configured properties of the given Connector template does not include `specification` property");
}
return specification;
}

/* default */ static ConfigurationProperty securityProperty(final PropertyData propertyData) {
final ConfigurationProperty property = property(propertyData);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import io.syndesis.model.connection.ConfigurationProperty;
import io.syndesis.model.connection.Connector;
import io.syndesis.model.connection.ConnectorTemplate;
import io.syndesis.model.connection.CustomConnector;

import org.junit.Test;

Expand Down Expand Up @@ -88,15 +89,15 @@ public void shouldCreateSecurityConfigurationFromReverbSwagger() throws IOExcept
.build())
.build();

final Connector template = new Connector.Builder()//
final CustomConnector customConnector = new CustomConnector.Builder()//
.id("reverb-api")//
.name("Reverb API")//
.description("Invokes Reverb API")//
.icon("fa-music")//
.putConfiguredProperty("specification", specification)//
.build();

final Connector sculpted = new SwaggerConnectorGenerator().generate(connectorTemplate, template);
final Connector sculpted = new SwaggerConnectorGenerator().generate(connectorTemplate, customConnector);

assertThat(sculpted.getProperties()).containsKeys("accessToken", "accessTokenUrl", "clientId", "clientSecret");
}
Expand Down Expand Up @@ -127,15 +128,15 @@ public void shouldSculptForConcurQuickExpensesSwagger() throws IOException {
.build())
.build();

final Connector template = new Connector.Builder()//
final CustomConnector customConnector = new CustomConnector.Builder()//
.id("concur-quick-expense")//
.name("Concur Quick Expense API")//
.description("Invokes Quick Expense API")//
.icon("fa-link")//
.putConfiguredProperty("specification", specification)//
.build();

final Connector sculpted = new SwaggerConnectorGenerator().generate(connectorTemplate, template);
final Connector sculpted = new SwaggerConnectorGenerator().generate(connectorTemplate, customConnector);

final Connector expected = Json.mapper().readValue(resource("/expected-quick-expenses-connector.json"),
Connector.class);
Expand Down
6 changes: 3 additions & 3 deletions rest/dao/src/main/resources/io/syndesis/dao/deployment.json
Original file line number Diff line number Diff line change
Expand Up @@ -860,13 +860,13 @@
"group": "producer",
"label": "producer",
"required": true,
"type": "string",
"type": "file",
"javaType": "java.lang.String",
"tags": [ "blob" ],
"deprecated": false,
"secret": false,
"componentProperty": true,
"description": "Swagger specification of the service"
"description": "Upload the swagger fore for your Custom API Client Connector. Custom API's are RESTful APIs and can be hosted anywhere, as long as a well-documented swagger is available and conforms to OpenAPI standards."
}
},
"connectorProperties": {
Expand Down Expand Up @@ -906,7 +906,7 @@
"required": false,
"type": "hidden",
"javaType": "java.lang.String",
"tags": [ "blob" ],
"tags": [ "upload", "url" ],
"deprecated": false,
"secret": false,
"componentProperty": true,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/**
* Copyright (C) 2016 Red Hat, Inc.
*
* Licensed under the Apache 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://www.apache.org/licenses/LICENSE-2.0
*
* 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 io.syndesis.model.connection;

import com.fasterxml.jackson.databind.annotation.JsonDeserialize;

import io.syndesis.model.WithId;
import io.syndesis.model.WithName;
import io.syndesis.model.WithProperties;

import org.immutables.value.Value;

@Value.Immutable
@JsonDeserialize(builder = CustomConnector.Builder.class)
@SuppressWarnings("immutables")
public interface CustomConnector extends WithId<CustomConnector>, WithName, WithProperties {

class Builder extends ImmutableCustomConnector.Builder {
// make ImmutableCustomConnector accessible
}

String getDescription();

String getIcon();

}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
import java.util.List;
import java.util.Map;

import javax.persistence.EntityNotFoundException;
import javax.validation.constraints.NotNull;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
Expand All @@ -30,15 +29,13 @@

import io.swagger.annotations.Api;
import io.swagger.annotations.ApiParam;
import io.syndesis.connector.generator.ConnectorGenerator;
import io.syndesis.credential.Credentials;
import io.syndesis.dao.manager.DataManager;
import io.syndesis.dao.manager.EncryptionComponent;
import io.syndesis.inspector.Inspectors;
import io.syndesis.model.Kind;
import io.syndesis.model.action.ConnectorAction;
import io.syndesis.model.connection.Connector;
import io.syndesis.model.connection.ConnectorTemplate;
import io.syndesis.model.filter.FilterOptions;
import io.syndesis.model.filter.Op;
import io.syndesis.rest.v1.handler.BaseHandler;
Expand All @@ -47,52 +44,27 @@
import io.syndesis.rest.v1.state.ClientSideState;
import io.syndesis.verifier.Verifier;

import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;

@Path("/connectors")
@Api(value = "connectors")
@Component
public class ConnectorHandler extends BaseHandler implements Lister<Connector>, Getter<Connector> {

private final ApplicationContext applicationContext;
private final Credentials credentials;
private final EncryptionComponent encryptionComponent;
private final Inspectors inspectors;
private final ClientSideState state;
private final Verifier verifier;

public ConnectorHandler(final DataManager dataMgr, final Verifier verifier, final Credentials credentials,
final Inspectors inspectors, final ClientSideState state, final EncryptionComponent encryptionComponent,
final ApplicationContext applicationContext) {
final Inspectors inspectors, final ClientSideState state, final EncryptionComponent encryptionComponent) {
super(dataMgr);
this.verifier = verifier;
this.credentials = credentials;
this.inspectors = inspectors;
this.state = state;
this.encryptionComponent = encryptionComponent;
this.applicationContext = applicationContext;
}

@POST
@Produces(MediaType.APPLICATION_JSON)
@Consumes("application/json")
@Path("/{connector-template-id}")
public Connector create(@NotNull @PathParam("connector-template-id") final String connectorTemplateId,
final Connector template) {
final ConnectorTemplate connectorTemplate = getDataManager().fetch(ConnectorTemplate.class,
connectorTemplateId);

if (connectorTemplate == null) {
throw new EntityNotFoundException("Connector template: " + connectorTemplateId);
}

final ConnectorGenerator connectorGenerator = applicationContext.getBean(connectorTemplateId,
ConnectorGenerator.class);

final Connector connector = connectorGenerator.generate(connectorTemplate, template);

return getDataManager().create(connector);
}

@Path("/{id}/credentials")
Expand Down
Loading

0 comments on commit 6a2d502

Please sign in to comment.