Java tutorial
/* * 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.net.URL; 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.core.Context; import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.ResponseBuilder; 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 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.bitbucket.BitbucketResourceResolver; import io.apicurio.hub.api.codegen.OpenApi2Thorntail; import io.apicurio.hub.api.codegen.OpenApi2Thorntail.ThorntailProjectSettings; 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.github.GitHubResourceResolver; import io.apicurio.hub.api.gitlab.GitLabResourceResolver; import io.apicurio.hub.api.metrics.IApiMetrics; 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.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.OpenApi2Document; import io.apicurio.hub.core.beans.OpenApi3Document; import io.apicurio.hub.core.beans.OpenApiDocument; import io.apicurio.hub.core.beans.OpenApiInfo; 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.js.OaiCommandException; import io.apicurio.hub.core.js.OaiCommandExecutor; import io.apicurio.hub.core.storage.IStorage; import io.apicurio.hub.core.storage.StorageException; import io.apicurio.hub.core.util.FormatUtils; /** * @author eric.wittmann@gmail.com */ @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 IStorage storage; @Inject private SourceConnectorFactory sourceConnectorFactory; @Inject private ISecurityContext security; @Inject private IApiMetrics metrics; @Inject private OaiCommandExecutor oaiCommandExecutor; @Inject private IEditingSessionManager editingSessionManager; @Context private HttpServletRequest request; @Context private HttpServletResponse response; /** * @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()); 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()); Date now = new Date(); String user = this.security.getCurrentUser().getLogin(); String description = resourceInfo.getDescription(); if (description == null) { description = ""; } ApiDesign design = new ApiDesign(); design.setName(resourceInfo.getName()); design.setDescription(description); design.setCreatedBy(user); design.setCreatedOn(now); design.setTags(resourceInfo.getTags()); try { String content = initialApiContent.getContent(); if (resourceInfo.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); } 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); ApiDesignResourceInfo resourceInfo = ApiDesignResourceInfo.fromContent(content); String name = resourceInfo.getName(); if (name == null) { name = "Imported API Design"; } Date now = new Date(); String user = this.security.getCurrentUser().getLogin(); ApiDesign design = new ApiDesign(); design.setName(name); design.setDescription(resourceInfo.getDescription()); design.setCreatedBy(user); design.setCreatedOn(now); design.setTags(resourceInfo.getTags()); try { if (resourceInfo.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); } metrics.apiImport(null); return design; } } catch (IOException e) { throw new ServerError(e); } catch (ApiValidationException ave) { throw ave; } 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); 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); } } if (name == null) { name = "Imported API Design"; } Date now = new Date(); String user = this.security.getCurrentUser().getLogin(); ApiDesign design = new ApiDesign(); design.setName(name); design.setDescription(resourceInfo.getDescription()); design.setCreatedBy(user); design.setCreatedOn(now); design.setTags(resourceInfo.getTags()); try { if (resourceInfo.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); } metrics.apiImport(null); return design; } } catch (ApiValidationException ave) { throw ave; } catch (IOException e) { throw new ServerError(e); } catch (Exception e) { throw new ServerError(e); } } /** * @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 (OAI document) OpenApiDocument doc; if (info.getSpecVersion() == null || info.getSpecVersion().equals("2.0")) { doc = new OpenApi2Document(); } else { doc = new OpenApi3Document(); } doc.setInfo(new OpenApiInfo()); doc.getInfo().setTitle(info.getName()); doc.getInfo().setDescription(info.getDescription()); doc.getInfo().setVersion("1.0.0"); String oaiContent = mapper.writeValueAsString(doc); // Create the API Design in the database String designId = storage.createApiDesign(user, design, oaiContent); design.setId(designId); metrics.apiCreate(info.getSpecVersion()); return design; } catch (JsonProcessingException | StorageException e) { throw new ServerError(e); } } /** * @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#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}", "PUT"); try { String user = this.security.getCurrentUser().getLogin(); logger.debug("\tUSER: {}", user); ApiDesignContent designContent = this.storage.getLatestContentDocument(user, designId); String content = designContent.getOaiDocument(); 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) */ @Override public Response getContent(String designId, String format) 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(); ApiDesignContent designContent = this.storage.getLatestContentDocument(user, designId); 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()); } String content = this.oaiCommandExecutor.executeCommands(designContent.getOaiDocument(), commands); String ct = "application/json; charset=" + StandardCharsets.UTF_8; String cl = null; // 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); 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 { 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.listApiDesignPublications(designId, 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 = info.toResourceUrl(); 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); } } /** * 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.getOrg())); data.set("team", JsonNodeFactory.instance.textNode(info.getOrg())); data.set("group", JsonNodeFactory.instance.textNode(info.getOrg())); data.set("project", JsonNodeFactory.instance.textNode(info.getOrg())); data.set("branch", JsonNodeFactory.instance.textNode(info.getOrg())); data.set("resource", JsonNodeFactory.instance.textNode(info.getOrg())); 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); } } /** * 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(); ApiDesignContent designContent = this.storage.getLatestContentDocument(user, designId); String content = designContent.getOaiDocument(); 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.getOaiDocument(), 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 oaiContent = this.getApiContent(designId, FormatType.JSON); // TODO support other types besides Thorntail if (project.getType() == CodegenProjectType.thorntail) { String groupId = project.getAttributes().get("groupId"); String artifactId = project.getAttributes().get("artifactId"); String javaPackage = project.getAttributes().get("javaPackage"); ThorntailProjectSettings settings = new ThorntailProjectSettings(); settings.groupId = groupId != null ? groupId : "org.example.api"; settings.artifactId = artifactId != null ? artifactId : "generated-api"; settings.javaPackage = javaPackage != null ? javaPackage : "org.example.api"; boolean updateOnly = "true".equals(project.getAttributes().get("update-only")); OpenApi2Thorntail generator = new OpenApi2Thorntail(); generator.setSettings(settings); generator.setOpenApiDocument(oaiContent); generator.setUpdateOnly(updateOnly); ByteArrayOutputStream stream = generator.generate(); byte[] data = stream.toByteArray(); String fname = settings.artifactId + ".zip"; ResponseBuilder builder = Response.ok().entity(data) .header("Content-Disposition", "attachment; filename=\"" + fname + "\"") .header("Content-Type", "application/zip") .header("Content-Length", String.valueOf(data.length)); return builder.build(); } else { throw new ServerError("Unsupported project type: " + project.getType()); } } catch (IOException | StorageException e) { throw new ServerError(e); } } /** * @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 oaiContent = this.getApiContent(project.getDesignId(), FormatType.JSON); // TODO support other types besides Thorntail if (project.getType() == CodegenProjectType.thorntail) { String groupId = project.getAttributes().get("groupId"); String artifactId = project.getAttributes().get("artifactId"); String javaPackage = project.getAttributes().get("javaPackage"); ThorntailProjectSettings settings = new ThorntailProjectSettings(); settings.groupId = groupId != null ? groupId : "org.example.api"; settings.artifactId = artifactId != null ? artifactId : "generated-api"; settings.javaPackage = javaPackage != null ? javaPackage : "org.example.api"; OpenApi2Thorntail generator = new OpenApi2Thorntail(); generator.setSettings(settings); generator.setOpenApiDocument(oaiContent); generator.setUpdateOnly(updateOnly); 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; } else { throw new ServerError("Unsupported project type: " + project.getType()); } } catch (IOException | SourceConnectorException e) { throw new ServerError(e); } } /** * 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 = BitbucketResourceResolver.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 = GitHubResourceResolver.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 = GitLabResourceResolver.create(group, proj, branch, path); } break; default: throw new RuntimeException("Unsupported type: " + scsType); } return url; } }