/* * Copyright 2017 JBoss 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.apicurio.hub.api.rest.impl; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.URL; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collection; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.UUID; import java.util.zip.ZipInputStream; import javax.enterprise.context.ApplicationScoped; import javax.inject.Inject; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.Context; import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.ResponseBuilder; import javax.ws.rs.core.StreamingOutput; import org.apache.commons.codec.binary.Base64; import org.apache.commons.io.IOUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.fasterxml.jackson.annotation.JsonInclude.Include; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.JsonNodeFactory; import com.fasterxml.jackson.databind.node.ObjectNode; import graphql.schema.idl.SchemaParser; import io.apicurio.datamodels.Library; import io.apicurio.datamodels.core.models.Document; import io.apicurio.datamodels.core.models.DocumentType; import io.apicurio.datamodels.core.models.ValidationProblem; import io.apicurio.datamodels.core.models.ValidationProblemSeverity; import io.apicurio.datamodels.core.validation.IValidationSeverityRegistry; import io.apicurio.datamodels.core.validation.ValidationRuleMetaData; import io.apicurio.hub.api.beans.CodegenLocation; import io.apicurio.hub.api.beans.ImportApiDesign; import io.apicurio.hub.api.beans.NewApiDesign; import io.apicurio.hub.api.beans.NewApiPublication; import io.apicurio.hub.api.beans.NewCodegenProject; import io.apicurio.hub.api.beans.ResourceContent; import io.apicurio.hub.api.beans.UpdateCodgenProject; import io.apicurio.hub.api.beans.UpdateCollaborator; import io.apicurio.hub.api.beans.ValidationError; import io.apicurio.hub.api.bitbucket.BitbucketResourceResolver; import io.apicurio.hub.api.codegen.OpenApi2JaxRs; import io.apicurio.hub.api.codegen.OpenApi2JaxRs.JaxRsProjectSettings; import io.apicurio.hub.api.codegen.OpenApi2Quarkus; import io.apicurio.hub.api.codegen.OpenApi2Thorntail; import io.apicurio.hub.api.connectors.ISourceConnector; import io.apicurio.hub.api.connectors.SourceConnectorException; import io.apicurio.hub.api.connectors.SourceConnectorFactory; import io.apicurio.hub.api.content.ContentDereferencer; import io.apicurio.hub.api.github.GitHubResourceResolver; import io.apicurio.hub.api.gitlab.GitLabResourceResolver; import io.apicurio.hub.api.metrics.IApiMetrics; import io.apicurio.hub.api.microcks.IMicrocksConnector; import io.apicurio.hub.api.microcks.MicrocksConnectorException; import io.apicurio.hub.api.rest.IDesignsResource; import io.apicurio.hub.api.security.ISecurityContext; import io.apicurio.hub.core.beans.ApiContentType; import io.apicurio.hub.core.beans.ApiDesign; import io.apicurio.hub.core.beans.ApiDesignChange; import io.apicurio.hub.core.beans.ApiDesignCollaborator; import io.apicurio.hub.core.beans.ApiDesignCommand; import io.apicurio.hub.core.beans.ApiDesignContent; import io.apicurio.hub.core.beans.ApiDesignResourceInfo; import io.apicurio.hub.core.beans.ApiDesignType; import io.apicurio.hub.core.beans.ApiMock; import io.apicurio.hub.core.beans.ApiPublication; import io.apicurio.hub.core.beans.CodegenProject; import io.apicurio.hub.core.beans.CodegenProjectType; import io.apicurio.hub.core.beans.Contributor; import io.apicurio.hub.core.beans.FormatType; import io.apicurio.hub.core.beans.Invitation; import io.apicurio.hub.core.beans.LinkedAccountType; import io.apicurio.hub.core.beans.MockReference; import io.apicurio.hub.core.beans.SharingConfiguration; import io.apicurio.hub.core.beans.SharingLevel; import io.apicurio.hub.core.beans.UpdateSharingConfiguration; import io.apicurio.hub.core.cmd.OaiCommandException; import io.apicurio.hub.core.cmd.OaiCommandExecutor; import io.apicurio.hub.core.config.HubConfiguration; import io.apicurio.hub.core.editing.IEditingSessionManager; import io.apicurio.hub.core.exceptions.AccessDeniedException; import io.apicurio.hub.core.exceptions.ApiValidationException; import io.apicurio.hub.core.exceptions.NotFoundException; import io.apicurio.hub.core.exceptions.ServerError; import io.apicurio.hub.core.storage.IStorage; import io.apicurio.hub.core.storage.StorageException; import io.apicurio.hub.core.util.FormatUtils; /** * @author [email protected] */ @ApplicationScoped public class DesignsResource implements IDesignsResource { private static Logger logger = LoggerFactory.getLogger(DesignsResource.class); private static ObjectMapper mapper = new ObjectMapper(); static { mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); mapper.setSerializationInclusion(Include.NON_NULL); } @Inject private HubConfiguration config; @Inject private IStorage storage; @Inject private SourceConnectorFactory sourceConnectorFactory; @Inject private ISecurityContext security; @Inject private IApiMetrics metrics; @Inject private OaiCommandExecutor oaiCommandExecutor; @Inject private ContentDereferencer dereferencer; @Inject private IEditingSessionManager editingSessionManager; @Inject private IMicrocksConnector microcks; @Context private HttpServletRequest request; @Context private HttpServletResponse response; @Inject private GitLabResourceResolver gitLabResolver; @Inject private GitHubResourceResolver gitHubResolver; @Inject private BitbucketResourceResolver bitbucketResolver; /** * @see io.apicurio.hub.api.rest.IDesignsResource#listDesigns() */ @Override public Collection<ApiDesign> listDesigns() throws ServerError { metrics.apiCall("/designs", "GET"); try { logger.debug("Listing API Designs"); String user = this.security.getCurrentUser().getLogin(); Collection<ApiDesign> designs = this.storage.listApiDesigns(user); return designs; } catch (StorageException e) { throw new ServerError(e); } } /** * @see io.apicurio.hub.api.rest.IDesignsResource#importDesign(io.apicurio.hub.api.beans.ImportApiDesign) */ @Override public ApiDesign importDesign(ImportApiDesign info) throws ServerError, NotFoundException, ApiValidationException { metrics.apiCall("/designs", "PUT"); if (info.getData() != null && !info.getData().trim().isEmpty()) { logger.debug("Importing an API Design (from data)."); return importDesignFromData(info); } else { logger.debug("Importing an API Design: {}", info.getUrl()); if (info.getUrl() == null) { throw new ApiValidationException("No data provided to import."); } ISourceConnector connector = null; try { connector = this.sourceConnectorFactory.createConnector(info.getUrl()); } catch (NotFoundException nfe) { // This means it's not a source control URL. So we'll treat it as a raw content URL. connector = null; } if (connector != null) { return importDesignFromSource(info, connector); } else { return importDesignFromUrl(info); } } } /** * Imports an API Design from one of the source control systems using its API. * @param info * @param connector * @throws NotFoundException * @throws ServerError * @throws ApiValidationException */ private ApiDesign importDesignFromSource(ImportApiDesign info, ISourceConnector connector) throws NotFoundException, ServerError, ApiValidationException { try { ApiDesignResourceInfo resourceInfo = connector.validateResourceExists(info.getUrl()); ResourceContent initialApiContent = connector.getResourceContent(info.getUrl()); ApiDesign design = doImport(resourceInfo, initialApiContent.getContent()); metrics.apiImport(connector.getType()); return design; } catch (SourceConnectorException | IOException e) { throw new ServerError(e); } } /** * Imports an API Design from base64 encoded content included in the request. This supports * the use-case where the UI allows the user to simply copy/paste the full API content. * @param info * @throws ServerError */ private ApiDesign importDesignFromData(ImportApiDesign info) throws ServerError, ApiValidationException { try { String data = info.getData(); byte[] decodedData = Base64.decodeBase64(data); try (InputStream is = new ByteArrayInputStream(decodedData)) { String content = IOUtils.toString(is, "UTF-8"); ApiDesignResourceInfo resourceInfo = ApiDesignResourceInfo.fromContent(content); if (resourceInfo == null) { throw new ApiValidationException("Failed to determine API Design type from content."); } ApiDesign design = doImport(resourceInfo, content); metrics.apiImport(null); return design; } } catch (ApiValidationException | ServerError e) { throw e; } catch (Exception e) { throw new ServerError(e); } } /** * Imports an API design from an arbitrary URL. This simply opens a connection to that * URL and tries to consume its content as an OpenAPI document. * @param info * @throws NotFoundException * @throws ServerError * @throws ApiValidationException */ private ApiDesign importDesignFromUrl(ImportApiDesign info) throws NotFoundException, ServerError, ApiValidationException { try { URL url = new URL(info.getUrl()); try (InputStream is = url.openStream()) { String content = IOUtils.toString(is, "UTF-8"); ApiDesignResourceInfo resourceInfo = ApiDesignResourceInfo.fromContent(content); String name = resourceInfo.getName(); if (name == null) { name = url.getPath(); if (name != null && name.indexOf("/") >= 0) { name = name.substring(name.indexOf("/") + 1); } } resourceInfo.setName(name); ApiDesign design = doImport(resourceInfo, content); metrics.apiImport(null); return design; } } catch (ApiValidationException | ServerError e) { throw e; } catch (Exception e) { throw new ServerError(e); } } /** * Common functionality when importing a design. * @param info * @param content * @throws NotFoundException * @throws ServerError * @throws ApiValidationException */ private ApiDesign doImport(ApiDesignResourceInfo info, String content) throws ServerError, IOException { Date now = new Date(); String user = this.security.getCurrentUser().getLogin(); if (info.getName() == null) { info.setName("Imported API Design"); } if (info.getDescription() == null) { info.setDescription(""); } ApiDesign design = new ApiDesign(); design.setName(info.getName()); design.setDescription(info.getDescription()); design.setCreatedBy(user); design.setCreatedOn(now); design.setTags(info.getTags()); design.setType(info.getType()); try { // Convert from YAML to JSON if the source is YAML (always store as JSON). Only for non-GraphQL designs. if (info.getType() != ApiDesignType.GraphQL && info.getFormat() == FormatType.YAML) { content = FormatUtils.yamlToJson(content); } String id = this.storage.createApiDesign(user, design, content); design.setId(id); } catch (StorageException e) { throw new ServerError(e); } return design; } /** * @see io.apicurio.hub.api.rest.IDesignsResource#createDesign(io.apicurio.hub.api.beans.NewApiDesign) */ @Override public ApiDesign createDesign(NewApiDesign info) throws ServerError { logger.debug("Creating an API Design: {}", info.getName()); metrics.apiCall("/designs", "POST"); try { Date now = new Date(); String user = this.security.getCurrentUser().getLogin(); // The API Design meta-data ApiDesign design = new ApiDesign(); design.setName(info.getName()); design.setDescription(info.getDescription()); design.setCreatedBy(user); design.setCreatedOn(now); // The API Design content ApiDesignType type; if (info.getType() != null) { type = info.getType(); } else { if (info.getSpecVersion() == null || info.getSpecVersion().equals("2.0")) { type = ApiDesignType.OpenAPI20; } else { type = ApiDesignType.OpenAPI30; } } design.setType(type); String content = createNewDocument(type, info.getName(), info.getDescription()); // Create the API Design in the database String designId = storage.createApiDesign(user, design, content); design.setId(designId); metrics.apiCreate(type); return design; } catch (StorageException e) { throw new ServerError(e); } } /** * Creates a new document. * @param type * @param name * @param description */ private String createNewDocument(ApiDesignType type, String name, String description) { Document doc = null; switch (type) { case AsyncAPI20: doc = Library.createDocument(DocumentType.asyncapi2); break; case OpenAPI20: doc = Library.createDocument(DocumentType.openapi2); break; case OpenAPI30: doc = Library.createDocument(DocumentType.openapi3); break; case GraphQL: return "# GraphQL Schema '" + name + "' created " + new Date(); } if (doc != null) { doc.info = doc.createInfo(); doc.info.title = name; doc.info.description = description; doc.info.version = "1.0.0"; return Library.writeDocumentToJSONString(doc); } throw new RuntimeException("Unhandled API design type: " + type); } /** * @see io.apicurio.hub.api.rest.IDesignsResource#getDesign(java.lang.String) */ @Override public ApiDesign getDesign(String designId) throws ServerError, NotFoundException { logger.debug("Getting an API design with ID {}", designId); metrics.apiCall("/designs/{designId}", "GET"); try { String user = this.security.getCurrentUser().getLogin(); ApiDesign design = this.storage.getApiDesign(user, designId); return design; } catch (StorageException e) { throw new ServerError(e); } } /** * @see io.apicurio.hub.api.rest.IDesignsResource#updateDesign(java.lang.String, java.io.InputStream) */ @Override public void updateDesign(String designId, InputStream content) throws ServerError, NotFoundException, ApiValidationException { logger.debug("Updating an API design with ID {}", designId); metrics.apiCall("/designs/{designId}", "PUT"); try { String user = this.security.getCurrentUser().getLogin(); ApiDesign design = this.storage.getApiDesign(user, designId); String encoding = "UTF8"; if (request != null) { encoding = request.getCharacterEncoding(); } String contentStr = IOUtils.toString(content, encoding); try { if (design.getType() == ApiDesignType.GraphQL) { SchemaParser schemaParser = new SchemaParser(); schemaParser.parse(contentStr); } else { Document doc = Library.readDocumentFromJSONString(contentStr); switch (design.getType()) { case AsyncAPI20: if (doc.getDocumentType() != DocumentType.asyncapi2) { throw new Exception("Expected AsyncAPI 2 content."); } break; case OpenAPI20: if (doc.getDocumentType() != DocumentType.openapi2) { throw new Exception("Expected OpenAPI 2 content."); } break; case OpenAPI30: if (doc.getDocumentType() != DocumentType.openapi3) { throw new Exception("Expected OpenAPI 3 content."); } break; default: break; } } } catch (Exception e) { throw new ApiValidationException("Content is invalid.", e); } storage.addContent(user, designId, ApiContentType.Document, contentStr); } catch (IOException | StorageException e) { throw new ServerError(e); } } /** * @see io.apicurio.hub.api.rest.IDesignsResource#editDesign(java.lang.String) */ @Override public Response editDesign(String designId) throws ServerError, NotFoundException { logger.debug("Editing an API Design with ID {}", designId); metrics.apiCall("/designs/{designId}/session", "GET"); try { String user = this.security.getCurrentUser().getLogin(); logger.debug("\tUSER: {}", user); ApiDesignContent designContent = this.storage.getLatestContentDocument(user, designId); String content = designContent.getDocument(); long contentVersion = designContent.getContentVersion(); String secret = this.security.getToken().substring(0, Math.min(64, this.security.getToken().length() - 1)); String sessionId = this.editingSessionManager.createSessionUuid(designId, user, secret, contentVersion); logger.debug("\tCreated Session ID: {}", sessionId); logger.debug("\t Secret: {}", secret); byte[] bytes = content.getBytes(StandardCharsets.UTF_8); String ct = "application/json; charset=" + StandardCharsets.UTF_8; String cl = String.valueOf(bytes.length); ResponseBuilder builder = Response.ok().entity(content) .header("X-Apicurio-EditingSessionUuid", sessionId) .header("X-Apicurio-ContentVersion", contentVersion) .header("Content-Type", ct) .header("Content-Length", cl); return builder.build(); } catch (StorageException e) { throw new ServerError(e); } } /** * @see io.apicurio.hub.api.rest.IDesignsResource#deleteDesign(java.lang.String) */ @Override public void deleteDesign(String designId) throws ServerError, NotFoundException { logger.debug("Deleting an API Design with ID {}", designId); metrics.apiCall("/designs/{designId}", "DELETE"); try { String user = this.security.getCurrentUser().getLogin(); this.storage.deleteApiDesign(user, designId); } catch (StorageException e) { throw new ServerError(e); } } /** * @see io.apicurio.hub.api.rest.IDesignsResource#getContributors(java.lang.String) */ @Override public Collection<Contributor> getContributors(String designId) throws ServerError, NotFoundException { logger.debug("Retrieving contributors list for design with ID: {}", designId); metrics.apiCall("/designs/{designId}/contributors", "GET"); try { String user = this.security.getCurrentUser().getLogin(); return this.storage.listContributors(user, designId); } catch (StorageException e) { throw new ServerError(e); } } /** * @see io.apicurio.hub.api.rest.IDesignsResource#getContent(java.lang.String, java.lang.String, java.lang.String) */ @Override public Response getContent(String designId, String format, String dereference) throws ServerError, NotFoundException { logger.debug("Getting content for API design with ID: {}", designId); metrics.apiCall("/designs/{designId}/content", "GET"); try { String user = this.security.getCurrentUser().getLogin(); String content = null; String ct = null; ApiDesign apiDesign = this.storage.getApiDesign(user, designId); if (apiDesign.getType() == ApiDesignType.GraphQL) { ApiDesignContent designContent = this.storage.getLatestContentDocument(user, designId); content = designContent.getDocument(); ct = "application/graphql; charset=" + StandardCharsets.UTF_8; if ("json".equals(format)) { // TODO: Convert from SDL to JSON throw new ServerError("Format 'JSON' not yet supported for GraphQL designs."); } } else { ApiDesignContent designContent = this.storage.getLatestContentDocument(user, designId); // Load and apply commands if any exist. List<ApiDesignCommand> apiCommands = this.storage.listContentCommands(user, designId, designContent.getContentVersion()); List<String> commands = new ArrayList<>(apiCommands.size()); for (ApiDesignCommand apiCommand : apiCommands) { commands.add(apiCommand.getCommand()); } content = this.oaiCommandExecutor.executeCommands(designContent.getDocument(), commands); ct = "application/json; charset=" + StandardCharsets.UTF_8; // If we should dereference the content, do that now. if ("true".equalsIgnoreCase(dereference)) { content = dereferencer.dereference(content); } // Convert to yaml if necessary if ("yaml".equals(format)) { content = FormatUtils.jsonToYaml(content); ct = "application/x-yaml; charset=" + StandardCharsets.UTF_8; } } byte[] bytes = content.getBytes(StandardCharsets.UTF_8); String cl = String.valueOf(bytes.length); ResponseBuilder builder = Response.ok().entity(content) .header("Content-Type", ct) .header("Content-Length", cl); return builder.build(); } catch (StorageException | OaiCommandException | IOException e) { throw new ServerError(e); } } /** * @see io.apicurio.hub.api.rest.IDesignsResource#createInvitation(java.lang.String) */ @Override public Invitation createInvitation(String designId) throws ServerError, NotFoundException, AccessDeniedException { logger.debug("Creating a collaboration invitation for API: {} ", designId); metrics.apiCall("/designs/{designId}/invitations", "POST"); try { String user = this.security.getCurrentUser().getLogin(); String username = this.security.getCurrentUser().getName(); String inviteId = UUID.randomUUID().toString(); ApiDesign design = this.storage.getApiDesign(user, designId); if (!this.storage.hasOwnerPermission(user, designId)) { throw new AccessDeniedException(); } this.storage.createCollaborationInvite(inviteId, designId, user, username, "collaborator", design.getName()); Invitation invite = new Invitation(); invite.setCreatedBy(user); invite.setCreatedOn(new Date()); invite.setDesignId(designId); invite.setInviteId(inviteId); invite.setStatus("pending"); return invite; } catch (StorageException e) { throw new ServerError(e); } } /** * @see io.apicurio.hub.api.rest.IDesignsResource#getInvitation(java.lang.String, java.lang.String) */ @Override public Invitation getInvitation(String designId, String inviteId) throws ServerError, NotFoundException { logger.debug("Retrieving a collaboration invitation for API: {} and inviteID: {}", designId, inviteId); metrics.apiCall("/designs/{designId}/invitations/{inviteId}", "GET"); try { return this.storage.getCollaborationInvite(designId, inviteId); } catch (StorageException e) { throw new ServerError(e); } } /** * @see io.apicurio.hub.api.rest.IDesignsResource#getInvitations(java.lang.String) */ @Override public Collection<Invitation> getInvitations(String designId) throws ServerError, NotFoundException { logger.debug("Retrieving all collaboration invitations for API: {}", designId); metrics.apiCall("/designs/{designId}/invitations", "GET"); try { String user = this.security.getCurrentUser().getLogin(); return this.storage.listCollaborationInvites(designId, user); } catch (StorageException e) { throw new ServerError(e); } } /** * @see io.apicurio.hub.api.rest.IDesignsResource#acceptInvitation(java.lang.String, java.lang.String) */ @Override public void acceptInvitation(String designId, String inviteId) throws ServerError, NotFoundException { logger.debug("Accepting an invitation to collaborate on an API: {}", designId); metrics.apiCall("/designs/{designId}/invitations", "PUT"); try { String user = this.security.getCurrentUser().getLogin(); Invitation invite = this.storage.getCollaborationInvite(designId, inviteId); if (this.storage.hasWritePermission(user, designId)) { throw new NotFoundException(); } boolean accepted = this.storage.updateCollaborationInviteStatus(inviteId, "pending", "accepted", user); if (!accepted) { throw new NotFoundException(); } this.storage.createPermission(designId, user, invite.getRole()); } catch (StorageException e) { throw new ServerError(e); } } /** * @see io.apicurio.hub.api.rest.IDesignsResource#rejectInvitation(java.lang.String, java.lang.String) */ @Override public void rejectInvitation(String designId, String inviteId) throws ServerError, NotFoundException { logger.debug("Rejecting an invitation to collaborate on an API: {}", designId); metrics.apiCall("/designs/{designId}/invitations", "DELETE"); try { String user = this.security.getCurrentUser().getLogin(); // This will ensure that the invitation exists for this designId. this.storage.getCollaborationInvite(designId, inviteId); boolean accepted = this.storage.updateCollaborationInviteStatus(inviteId, "pending", "rejected", user); if (!accepted) { throw new NotFoundException(); } } catch (StorageException e) { throw new ServerError(e); } } /** * @see io.apicurio.hub.api.rest.IDesignsResource#getCollaborators(java.lang.String) */ @Override public Collection<ApiDesignCollaborator> getCollaborators(String designId) throws ServerError, NotFoundException { logger.debug("Retrieving all collaborators for API: {}", designId); metrics.apiCall("/designs/{designId}/collaborators", "GET"); try { String user = this.security.getCurrentUser().getLogin(); if (!this.storage.hasWritePermission(user, designId)) { throw new NotFoundException(); } return this.storage.listPermissions(designId); } catch (StorageException e) { throw new ServerError(e); } } /** * @see io.apicurio.hub.api.rest.IDesignsResource#updateCollaborator(java.lang.String, java.lang.String, io.apicurio.hub.api.beans.UpdateCollaborator) */ @Override public void updateCollaborator(String designId, String userId, UpdateCollaborator update) throws ServerError, NotFoundException, AccessDeniedException { logger.debug("Updating collaborator for API: {}", designId); metrics.apiCall("/designs/{designId}/collaborators/{userId}", "PUT"); try { String user = this.security.getCurrentUser().getLogin(); if (!this.storage.hasOwnerPermission(user, designId)) { throw new AccessDeniedException(); } this.storage.updatePermission(designId, userId, update.getNewRole()); } catch (StorageException e) { throw new ServerError(e); } } /** * @see io.apicurio.hub.api.rest.IDesignsResource#deleteCollaborator(java.lang.String, java.lang.String) */ @Override public void deleteCollaborator(String designId, String userId) throws ServerError, NotFoundException, AccessDeniedException { logger.debug("Deleting/revoking collaborator for API: {}", designId); metrics.apiCall("/designs/{designId}/collaborators/{userId}", "DELETE"); try { String user = this.security.getCurrentUser().getLogin(); if (!this.storage.hasOwnerPermission(user, designId)) { throw new AccessDeniedException(); } this.storage.deletePermission(designId, userId); } catch (StorageException e) { throw new ServerError(e); } } /** * @see io.apicurio.hub.api.rest.IDesignsResource#getActivity(java.lang.String, java.lang.Integer, java.lang.Integer) */ @Override public Collection<ApiDesignChange> getActivity(String designId, Integer start, Integer end) throws ServerError, NotFoundException { int from = 0; int to = 20; if (start != null) { from = start.intValue(); } if (end != null) { to = end.intValue(); } try { if (!config.isShareForEveryone()) { String user = this.security.getCurrentUser().getLogin(); if (!this.storage.hasWritePermission(user, designId)) { throw new NotFoundException(); } } return this.storage.listApiDesignActivity(designId, from, to); } catch (StorageException e) { throw new ServerError(e); } } /** * @see io.apicurio.hub.api.rest.IDesignsResource#getPublications(java.lang.String, java.lang.Integer, java.lang.Integer) */ @Override public Collection<ApiPublication> getPublications(String designId, Integer start, Integer end) throws ServerError, NotFoundException { int from = 0; int to = 20; if (start != null) { from = start.intValue(); } if (end != null) { to = end.intValue(); } try { String user = this.security.getCurrentUser().getLogin(); if (!this.storage.hasWritePermission(user, designId)) { throw new NotFoundException(); } return this.storage.listApiDesignPublicationsBy(designId, user, from, to); } catch (StorageException e) { throw new ServerError(e); } } /** * @see io.apicurio.hub.api.rest.IDesignsResource#publishApi(java.lang.String, io.apicurio.hub.api.beans.NewApiPublication) */ @Override public void publishApi(String designId, NewApiPublication info) throws ServerError, NotFoundException { LinkedAccountType type = info.getType(); try { // First step - publish the content to the soruce control system ISourceConnector connector = this.sourceConnectorFactory.createConnector(type); String resourceUrl = toResourceUrl(info); String formattedContent = getApiContent(designId, info.getFormat()); try { ResourceContent content = connector.getResourceContent(resourceUrl); content.setContent(formattedContent); connector.updateResourceContent(resourceUrl, info.getCommitMessage(), null, content); } catch (NotFoundException nfe) { connector.createResourceContent(resourceUrl, info.getCommitMessage(), formattedContent); } // Followup step - store a row in the api_content table try { String user = this.security.getCurrentUser().getLogin(); String publicationData = createPublicationData(info); storage.addContent(user, designId, ApiContentType.Publish, publicationData); } catch (Exception e) { logger.error("Failed to record API publication in database.", e); } } catch (SourceConnectorException e) { throw new ServerError(e); } } /** * @see io.apicurio.hub.api.rest.IDesignsResource#mockApi(java.lang.String) */ @Override public MockReference mockApi(String designId) throws ServerError, NotFoundException { try { // First step - publish the content to the Microcks server API String content = getApiContent(designId, FormatType.YAML); String serviceRef = this.microcks.uploadResourceContent(content); // Build mockURL from microcksURL. String mockURL = null; String microcksURL = config.getMicrocksApiUrl(); try { mockURL = microcksURL.substring(0, microcksURL.indexOf("/api")) + "/#/services/" + URLEncoder.encode(serviceRef, "UTF-8"); } catch (Exception e) { logger.error("Failed to produce a valid mockURL", e); } // Followup step - store a row in the api_content table try { String user = this.security.getCurrentUser().getLogin(); String mockData = createMockData(serviceRef, mockURL); storage.addContent(user, designId, ApiContentType.Mock, mockData); } catch (Exception e) { logger.error("Failed to record API mock publication in database.", e); } // Finally return response. MockReference mockRef = new MockReference(); mockRef.setMockType("microcks"); mockRef.setServiceRef(serviceRef); mockRef.setMockURL(mockURL); return mockRef; } catch (MicrocksConnectorException e) { throw new ServerError(e); } } /** * @see io.apicurio.hub.api.rest.IDesignsResource#getMocks(java.lang.String, java.lang.Integer, java.lang.Integer) */ @Override public Collection<ApiMock> getMocks(String designId, Integer start, Integer end) throws ServerError, NotFoundException { int from = 0; int to = 20; if (start != null) { from = start.intValue(); } if (end != null) { to = end.intValue(); } try { String user = this.security.getCurrentUser().getLogin(); if (!this.storage.hasWritePermission(user, designId)) { throw new NotFoundException(); } return this.storage.listApiDesignMocks(designId, from, to); } catch (StorageException e) { throw new ServerError(e); } } /** * Creates the JSON data to be stored in the data row representing a "publish API" event * (also known as an API publication). * @param info */ private String createPublicationData(NewApiPublication info) { try { ObjectMapper mapper = new ObjectMapper(); ObjectNode data = JsonNodeFactory.instance.objectNode(); data.set("type", JsonNodeFactory.instance.textNode(info.getType().name())); data.set("org", JsonNodeFactory.instance.textNode(info.getOrg())); data.set("repo", JsonNodeFactory.instance.textNode(info.getRepo())); data.set("team", JsonNodeFactory.instance.textNode(info.getTeam())); data.set("group", JsonNodeFactory.instance.textNode(info.getGroup())); data.set("project", JsonNodeFactory.instance.textNode(info.getProject())); data.set("branch", JsonNodeFactory.instance.textNode(info.getBranch())); data.set("resource", JsonNodeFactory.instance.textNode(info.getResource())); data.set("format", JsonNodeFactory.instance.textNode(info.getFormat().name())); data.set("commitMessage", JsonNodeFactory.instance.textNode(info.getCommitMessage())); return mapper.writeValueAsString(data); } catch (JsonProcessingException e) { throw new RuntimeException(e); } } /** * Create the JSON data to be stored in the data row representing a "mock API" event * (also know as an API mock publication). * @param serviceRef The service reference as returned by Microcks * @param mockURL The URL for accessing description page on Microcks server */ private String createMockData(String serviceRef, String mockURL) { try { ObjectMapper mapper = new ObjectMapper(); ObjectNode data = JsonNodeFactory.instance.objectNode(); data.set("mockType", JsonNodeFactory.instance.textNode("microcks")); data.set("serviceRef", JsonNodeFactory.instance.textNode(serviceRef)); data.set("mockURL", JsonNodeFactory.instance.textNode(mockURL)); return mapper.writeValueAsString(data); } catch (JsonProcessingException e) { throw new RuntimeException(e); } } /** * Gets the current content of an API. * @param designId * @param format * @throws ServerError * @throws NotFoundException */ private String getApiContent(String designId, FormatType format) throws ServerError, NotFoundException { try { String user = this.security.getCurrentUser().getLogin(); ApiDesign design = this.storage.getApiDesign(user, designId); ApiDesignContent designContent = this.storage.getLatestContentDocument(user, designId); String content = designContent.getDocument(); if (design.getType() == ApiDesignType.GraphQL) { if (format != null && format != FormatType.SDL) { throw new ServerError("Unsupported format: " + format); } } else { List<ApiDesignCommand> apiCommands = this.storage.listContentCommands(user, designId, designContent.getContentVersion()); if (!apiCommands.isEmpty()) { List<String> commands = new ArrayList<>(apiCommands.size()); for (ApiDesignCommand apiCommand : apiCommands) { commands.add(apiCommand.getCommand()); } content = this.oaiCommandExecutor.executeCommands(designContent.getDocument(), commands); } // Convert to yaml if necessary if (format == FormatType.YAML) { content = FormatUtils.jsonToYaml(content); } else { content = FormatUtils.formatJson(content); } } return content; } catch (StorageException | OaiCommandException | IOException e) { throw new ServerError(e); } } /** * @see io.apicurio.hub.api.rest.IDesignsResource#getCodegenProjects(java.lang.String) */ @Override public Collection<CodegenProject> getCodegenProjects(String designId) throws ServerError, NotFoundException { logger.debug("Retrieving codegen project list for design with ID: {}", designId); metrics.apiCall("/designs/{designId}/codegen/projects", "GET"); try { String user = this.security.getCurrentUser().getLogin(); ApiDesign design = this.storage.getApiDesign(user, designId); return this.storage.listCodegenProjects(user, design.getId()); } catch (StorageException e) { throw new ServerError(e); } } /** * @see io.apicurio.hub.api.rest.IDesignsResource#createCodegenProject(java.lang.String, io.apicurio.hub.api.beans.NewCodegenProject) */ @Override public CodegenProject createCodegenProject(String designId, NewCodegenProject body) throws ServerError, NotFoundException, AccessDeniedException { logger.debug("Creating a codegen project for API: {} ", designId); metrics.apiCall("/designs/{designId}/codegen/projects", "POST"); try { String user = this.security.getCurrentUser().getLogin(); ApiDesign design = this.storage.getApiDesign(user, designId); if (!this.storage.hasWritePermission(user, designId)) { throw new AccessDeniedException(); } CodegenProject project = new CodegenProject(); Date now = new Date(); project.setCreatedBy(user); project.setCreatedOn(now); project.setModifiedBy(user); project.setModifiedOn(now); project.setDesignId(design.getId()); project.setType(body.getProjectType()); project.setAttributes(new HashMap<String, String>()); if (body.getProjectConfig() != null) { project.getAttributes().putAll(body.getProjectConfig()); } project.getAttributes().put("location", body.getLocation().toString()); project.getAttributes().put("update-only", Boolean.FALSE.toString()); if (body.getPublishInfo() != null) { if (body.getPublishInfo().getType() != null) { project.getAttributes().put("publish-type", body.getPublishInfo().getType().toString()); } project.getAttributes().put("publish-branch", body.getPublishInfo().getBranch()); project.getAttributes().put("publish-commitMessage", body.getPublishInfo().getCommitMessage()); project.getAttributes().put("publish-group", body.getPublishInfo().getGroup()); project.getAttributes().put("publish-location", body.getPublishInfo().getLocation()); project.getAttributes().put("publish-org", body.getPublishInfo().getOrg()); project.getAttributes().put("publish-project", body.getPublishInfo().getProject()); project.getAttributes().put("publish-repo", body.getPublishInfo().getRepo()); project.getAttributes().put("publish-team", body.getPublishInfo().getTeam()); } if (body.getLocation() == CodegenLocation.download) { // Nothing extra to do when downloading - that will be handled by a separate call } if (body.getLocation() == CodegenLocation.sourceControl) { String prUrl = generateAndPublishProject(project, false); project.getAttributes().put("pullRequest-url", prUrl); } String projectId = this.storage.createCodegenProject(user, project); project.setId(projectId); return project; } catch (StorageException e) { throw new ServerError(e); } } /** * @see io.apicurio.hub.api.rest.IDesignsResource#getCodegenProjectAsZip(java.lang.String, java.lang.String) */ @Override public Response getCodegenProjectAsZip(String designId, String projectId) throws ServerError, NotFoundException, AccessDeniedException { logger.debug("Downloading a codegen project for API Design with ID {}", designId); metrics.apiCall("/designs/{designId}/codegen/projects/{projectId}/zip", "GET"); String user = this.security.getCurrentUser().getLogin(); try { if (!this.storage.hasWritePermission(user, designId)) { throw new AccessDeniedException(); } CodegenProject project = this.storage.getCodegenProject(user, designId, projectId); String content = this.getApiContent(designId, FormatType.JSON); // TODO support other types besides Thorntail if (project.getType() == CodegenProjectType.thorntail) { JaxRsProjectSettings settings = toJaxRsSettings(project); boolean updateOnly = "true".equals(project.getAttributes().get("update-only")); final OpenApi2Thorntail generator = new OpenApi2Thorntail(); generator.setSettings(settings); generator.setOpenApiDocument(content); generator.setUpdateOnly(updateOnly); return asResponse(settings, generator); } else if (project.getType() == CodegenProjectType.jaxrs) { JaxRsProjectSettings settings = toJaxRsSettings(project); boolean updateOnly = "true".equals(project.getAttributes().get("update-only")); final OpenApi2JaxRs generator = new OpenApi2JaxRs(); generator.setSettings(settings); generator.setOpenApiDocument(content); generator.setUpdateOnly(updateOnly); return asResponse(settings, generator); } else if (project.getType() == CodegenProjectType.quarkus) { JaxRsProjectSettings settings = toJaxRsSettings(project); boolean updateOnly = "true".equals(project.getAttributes().get("update-only")); final OpenApi2Quarkus generator = new OpenApi2Quarkus(); generator.setSettings(settings); generator.setOpenApiDocument(content); generator.setUpdateOnly(updateOnly); return asResponse(settings, generator); } else { throw new ServerError("Unsupported project type: " + project.getType()); } } catch (StorageException e) { throw new ServerError(e); } } /** * Generates the project and returns the result as a streaming response. * @param settings * @param generator */ private Response asResponse(JaxRsProjectSettings settings, final OpenApi2JaxRs generator) { StreamingOutput stream = new StreamingOutput() { @Override public void write(OutputStream output) throws IOException, WebApplicationException { generator.generate(output); } }; String fname = settings.artifactId + ".zip"; ResponseBuilder builder = Response.ok().entity(stream) .header("Content-Disposition", "attachment; filename=\"" + fname + "\"") .header("Content-Type", "application/zip"); return builder.build(); } /** * @see io.apicurio.hub.api.rest.IDesignsResource#updateCodegenProject(java.lang.String, java.lang.String, io.apicurio.hub.api.beans.UpdateCodgenProject) */ @Override public CodegenProject updateCodegenProject(String designId, String projectId, UpdateCodgenProject body) throws ServerError, NotFoundException, AccessDeniedException { logger.debug("Updating codegen project for API: {}", designId); metrics.apiCall("/designs/{designId}/codegen/projects/{projectId}", "PUT"); try { String user = this.security.getCurrentUser().getLogin(); if (!this.storage.hasWritePermission(user, designId)) { throw new AccessDeniedException(); } CodegenProject project = this.storage.getCodegenProject(user, designId, projectId); project.setType(body.getProjectType()); project.setAttributes(new HashMap<String, String>()); if (body.getProjectConfig() != null) { project.getAttributes().putAll(body.getProjectConfig()); } project.getAttributes().put("location", body.getLocation().toString()); project.getAttributes().put("update-only", Boolean.TRUE.toString()); if (body.getPublishInfo() != null) { if (body.getPublishInfo().getType() != null) { project.getAttributes().put("publish-type", body.getPublishInfo().getType().toString()); } project.getAttributes().put("publish-branch", body.getPublishInfo().getBranch()); project.getAttributes().put("publish-commitMessage", body.getPublishInfo().getCommitMessage()); project.getAttributes().put("publish-group", body.getPublishInfo().getGroup()); project.getAttributes().put("publish-location", body.getPublishInfo().getLocation()); project.getAttributes().put("publish-org", body.getPublishInfo().getOrg()); project.getAttributes().put("publish-project", body.getPublishInfo().getProject()); project.getAttributes().put("publish-repo", body.getPublishInfo().getRepo()); project.getAttributes().put("publish-team", body.getPublishInfo().getTeam()); } if (body.getLocation() == CodegenLocation.download) { // Nothing extra to do when downloading - that will be handled by a separate call } if (body.getLocation() == CodegenLocation.sourceControl) { String prUrl = generateAndPublishProject(project, true); project.getAttributes().put("pullRequest-url", prUrl); } this.storage.updateCodegenProject(user, project); return project; } catch (StorageException e) { throw new ServerError(e); } } /** * @see io.apicurio.hub.api.rest.IDesignsResource#deleteCodegenProject(java.lang.String, java.lang.String) */ @Override public void deleteCodegenProject(String designId, String projectId) throws ServerError, NotFoundException, AccessDeniedException { logger.debug("Deleting codegen project for API: {}", designId); metrics.apiCall("/designs/{designId}/codegen/projects/{projectId}", "DELETE"); try { String user = this.security.getCurrentUser().getLogin(); if (!this.storage.hasWritePermission(user, designId)) { throw new AccessDeniedException(); } this.storage.deleteCodegenProject(user, designId, projectId); } catch (StorageException e) { throw new ServerError(e); } } /** * @see io.apicurio.hub.api.rest.IDesignsResource#deleteCodegenProjects(java.lang.String) */ @Override public void deleteCodegenProjects(String designId) throws ServerError, NotFoundException, AccessDeniedException { logger.debug("Deleting ALL codegen projects for API: {}", designId); metrics.apiCall("/designs/{designId}/codegen/projects", "DELETE"); try { String user = this.security.getCurrentUser().getLogin(); if (!this.storage.hasWritePermission(user, designId)) { throw new AccessDeniedException(); } this.storage.deleteCodegenProjects(user, designId); } catch (StorageException e) { throw new ServerError(e); } } /** * Generate and publish (to a git/source control system) a project. This will * generate a project from the OpenAPI document and then publish the result to * a soruce control platform. * @param project * @param updateOnly * @return the URL of the published pull request */ private String generateAndPublishProject(CodegenProject project, boolean updateOnly) throws ServerError, NotFoundException { try { String content = this.getApiContent(project.getDesignId(), FormatType.JSON); // TODO support other types besides JAX-RS if (project.getType() == CodegenProjectType.thorntail) { JaxRsProjectSettings settings = toJaxRsSettings(project); OpenApi2Thorntail generator = new OpenApi2Thorntail(); generator.setSettings(settings); generator.setOpenApiDocument(content); generator.setUpdateOnly(updateOnly); return generateAndPublish(project, generator); } else if (project.getType() == CodegenProjectType.jaxrs) { JaxRsProjectSettings settings = toJaxRsSettings(project); OpenApi2JaxRs generator = new OpenApi2JaxRs(); generator.setSettings(settings); generator.setOpenApiDocument(content); generator.setUpdateOnly(updateOnly); return generateAndPublish(project, generator); } else if (project.getType() == CodegenProjectType.quarkus) { JaxRsProjectSettings settings = toJaxRsSettings(project); OpenApi2Quarkus generator = new OpenApi2Quarkus(); generator.setSettings(settings); generator.setOpenApiDocument(content); generator.setUpdateOnly(updateOnly); return generateAndPublish(project, generator); } else { throw new ServerError("Unsupported project type: " + project.getType()); } } catch (IOException | SourceConnectorException e) { throw new ServerError(e); } } /** * Generates the project and publishes the result to e.g. GitHub. * @param project * @param generator * @throws IOException * @throws NotFoundException * @throws SourceConnectorException */ private String generateAndPublish(CodegenProject project, OpenApi2JaxRs generator) throws IOException, NotFoundException, SourceConnectorException { ByteArrayOutputStream generatedContent = generator.generate(); LinkedAccountType scsType = LinkedAccountType.valueOf(project.getAttributes().get("publish-type")); ISourceConnector connector = this.sourceConnectorFactory.createConnector(scsType); String url = toSourceResourceUrl(project); String commitMessage = project.getAttributes().get("publish-commitMessage"); String pullRequestUrl = connector.createPullRequestFromZipContent(url, commitMessage, new ZipInputStream(new ByteArrayInputStream(generatedContent.toByteArray()))); return pullRequestUrl; } /** * Reads JAX-RS project settings from the project. * @param project */ private JaxRsProjectSettings toJaxRsSettings(CodegenProject project) { boolean codeOnly = "true".equals(project.getAttributes().get("codeOnly")); boolean reactive = "true".equals(project.getAttributes().get("reactive")); String groupId = project.getAttributes().get("groupId"); String artifactId = project.getAttributes().get("artifactId"); String javaPackage = project.getAttributes().get("javaPackage"); JaxRsProjectSettings settings = new JaxRsProjectSettings(); settings.codeOnly = codeOnly; settings.reactive = reactive; settings.groupId = groupId != null ? groupId : "org.example.api"; settings.artifactId = artifactId != null ? artifactId : "generated-api"; settings.javaPackage = javaPackage != null ? javaPackage : "org.example.api"; return settings; } /** * Creates a source control resource URL from the information found in the codegen project. * @param project */ private String toSourceResourceUrl(CodegenProject project) { LinkedAccountType scsType = LinkedAccountType.valueOf(project.getAttributes().get("publish-type")); String url; switch (scsType) { case Bitbucket: { String team = project.getAttributes().get("publish-team"); String repo = project.getAttributes().get("publish-repo"); String branch = project.getAttributes().get("publish-branch"); String path = project.getAttributes().get("publish-location"); url = bitbucketResolver.create(team, repo, branch, path); } break; case GitHub: { String org = project.getAttributes().get("publish-org"); String repo = project.getAttributes().get("publish-repo"); String branch = project.getAttributes().get("publish-branch"); String path = project.getAttributes().get("publish-location"); url = gitHubResolver.create(org, repo, branch, path); } break; case GitLab: { String group = project.getAttributes().get("publish-group"); String proj = project.getAttributes().get("publish-project"); String branch = project.getAttributes().get("publish-branch"); String path = project.getAttributes().get("publish-location"); url = gitLabResolver.create(group, proj, branch, path); } break; default: throw new RuntimeException("Unsupported type: " + scsType); } return url; } /** * Uses the information in the bean to create a resource URL. */ private String toResourceUrl(NewApiPublication info) { if (info.getType() == LinkedAccountType.GitHub) { return gitHubResolver.create(info.getOrg(), info.getRepo(), info.getBranch(), info.getResource()); } if (info.getType() == LinkedAccountType.GitLab) { return gitLabResolver.create(info.getGroup(), info.getProject(), info.getBranch(), info.getResource()); } if (info.getType() == LinkedAccountType.Bitbucket) { return bitbucketResolver.create(info.getTeam(), info.getRepo(), info.getBranch(), info.getResource()); } return null; } /** * @see io.apicurio.hub.api.rest.IDesignsResource#validateDesign(java.lang.String) */ @Override public List<ValidationError> validateDesign(String designId) throws ServerError, NotFoundException { logger.debug("Validating API design with ID: {}", designId); metrics.apiCall("/designs/{designId}/validation", "GET"); // TODO support validation of GraphQL APIs. String content = this.getApiContent(designId, FormatType.JSON); Document doc = Library.readDocumentFromJSONString(content); List<ValidationProblem> problems = Library.validate(doc, new IValidationSeverityRegistry() { @Override public ValidationProblemSeverity lookupSeverity(ValidationRuleMetaData rule) { return ValidationProblemSeverity.high; } }); List<ValidationError> errors = new ArrayList<>(); for (ValidationProblem problem : problems) { errors.add(new ValidationError(problem.errorCode, problem.nodePath.toString(), problem.property, problem.message, problem.severity.name())); } return errors; } /** * @see io.apicurio.hub.api.rest.IDesignsResource#configureSharing(java.lang.String, io.apicurio.hub.core.beans.UpdateSharingConfiguration) */ @Override public SharingConfiguration configureSharing(String designId, UpdateSharingConfiguration config) throws ServerError, NotFoundException { logger.debug("Configuring sharing settings for API: {} ", designId); metrics.apiCall("/designs/{designId}/sharing", "PUT"); try { String user = this.security.getCurrentUser().getLogin(); String uuid = UUID.randomUUID().toString(); // Note: only used if this is the first time if (!this.storage.hasOwnerPermission(user, designId)) { throw new NotFoundException(); } this.storage.setSharingConfig(designId, uuid, config.getLevel()); return this.storage.getSharingConfig(designId); } catch (StorageException e) { throw new ServerError(e); } } /** * @see io.apicurio.hub.api.rest.IDesignsResource#getSharingConfiguration(java.lang.String) */ @Override public SharingConfiguration getSharingConfiguration(String designId) throws ServerError, NotFoundException { logger.debug("Getting sharing settings for API: {} ", designId); metrics.apiCall("/designs/{designId}/sharing", "GET"); // Make sure we have access to the design. this.getDesign(designId); try { SharingConfiguration sharingConfig = this.storage.getSharingConfig(designId); if (sharingConfig == null) { sharingConfig = new SharingConfiguration(); sharingConfig.setLevel(SharingLevel.NONE); } return sharingConfig; } catch (StorageException e) { throw new ServerError(e); } } }