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

io.github.microcks.util.postman.PostmanCollectionImporter Maven / Gradle / Ivy

There is a newer version: 1.11.0
Show newest version
/*
 * Licensed to Laurent Broudoux (the "Author") under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. Author licenses this
 * file to you 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.github.microcks.util.postman;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.github.microcks.util.DispatchCriteriaHelper;
import io.github.microcks.util.DispatchStyles;
import io.github.microcks.util.MockRepositoryImportException;
import io.github.microcks.util.MockRepositoryImporter;
import io.github.microcks.domain.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.*;
import java.util.stream.Collectors;

/**
 * Implement of MockRepositoryImporter that uses a Postman collection for building
 * domain objects. Only v2 collection format is supported.
 * @author laurent
 */
public class PostmanCollectionImporter implements MockRepositoryImporter {

   /** A simple logger for diagnostic messages. */
   private static Logger log = LoggerFactory.getLogger(PostmanCollectionImporter.class);

   /** Postman collection property that references service version property. */
   public static final String SERVICE_VERSION_PROPERTY = "version";

   private JsonNode collection;
   // Flag telling if V2 format is used.
   private boolean isV2Collection = false;


   /**
    * Build a new importer.
    * @param collectionFilePath The path to local Postman collection file
    * @throws IOException if project file cannot be found or read.
    */
   public PostmanCollectionImporter(String collectionFilePath) throws IOException {
      try {
         // Read Json bytes.
         byte[] jsonBytes = Files.readAllBytes(Paths.get(collectionFilePath));
         // Convert them to Node using Jackson object mapper.
         ObjectMapper mapper = new ObjectMapper();
         collection = mapper.readTree(jsonBytes);
      } catch (Exception e) {
         log.error("Exception while parsing Postman collection file " + collectionFilePath, e);
         throw new IOException("Postman collection file parsing error");
      }
   }

   @Override
   public List getServiceDefinitions() throws MockRepositoryImportException {
      List result = new ArrayList<>();

      // Build a new service.
      Service service = new Service();

      // Collection V2 as an info node.
      if (collection.has("info")) {
         isV2Collection = true;
         fillServiceDefinition(service);
      } else {
         throw new MockRepositoryImportException("Only Postman v2 Collection are supported.");
      }

      // Then build its operations.
      try {
         service.setOperations(extractOperations());
      } catch (Throwable t) {
         log.error("Runtime exception while extracting Operations for " + service.getName());
         throw new MockRepositoryImportException("Runtime exception for " + service.getName() + ": " + t.getMessage());
      }

      result.add(service);
      return result;
   }

   private void fillServiceDefinition(Service service) throws MockRepositoryImportException {
      service.setName(collection.path("info").path("name").asText());
      service.setType(ServiceType.REST);

      String version = null;

      // On v2.1 collection format, we may have a version attribute under info.
      // See https://schema.getpostman.com/json/collection/v2.1.0/docs/index.html
      if (collection.path("info").has("version")) {
         version = collection.path("info").path("version").asText();
      } else {
         String description = collection.path("info").path("description").asText();
         if (description != null && description.indexOf(SERVICE_VERSION_PROPERTY + "=") != -1) {
            description = description.substring(description.indexOf(SERVICE_VERSION_PROPERTY + "="));
            description = description.substring(0, description.indexOf(' '));
            if (description.split("=").length > 1) {
               version = description.split("=")[1];
            }
         }
      }

      if (version == null){
         log.error("Version property is missing in Collection. Use either 'version' for v2.1 Collection or 'version=x.y - something' description syntax.");
         throw new MockRepositoryImportException("Version property is missing in Collection description");
      }
      service.setVersion(version);
   }

   @Override
   public List getResourceDefinitions(Service service) {
      List results = new ArrayList<>();
      // Non-sense on Postman collection. Just return empty result.
      return results;
   }

   @Override
   public List getMessageDefinitions(Service service, Operation operation) throws MockRepositoryImportException {

      if (isV2Collection) {
         return getMessageDefinitionsV2(service, operation);
      } else {
         throw new MockRepositoryImportException("Only Postman v2 Collection are supported.");
      }
   }

   private List getMessageDefinitionsV2(Service service, Operation operation) {
      Map result = new HashMap();

      Iterator items = collection.path("item").elements();
      while (items.hasNext()) {
         JsonNode item = items.next();
         result.putAll(getMessageDefinitionsV2("", item, operation));
      }

      // Adapt map to list of Exchanges.
      return result.entrySet().stream()
            .map(entry -> new RequestResponsePair(entry.getKey(), entry.getValue()))
            .collect(Collectors.toList());
   }

   private Map getMessageDefinitionsV2(String folderName, JsonNode itemNode, Operation operation) {
      log.debug("Extracting message definitions in folder " + folderName);
      Map result = new HashMap();
      String itemNodeName = itemNode.path("name").asText();

      if (!itemNode.has("request")) {
         // Item is simply a folder that may contain some other folders recursively.
         Iterator items = itemNode.path("item").elements();
         while (items.hasNext()) {
            JsonNode item = items.next();
            result.putAll(getMessageDefinitionsV2(folderName + "/" + itemNodeName, item, operation));
         }
      } else {
         // Item is here an operation description.
         String operationName = buildOperationName(itemNode, folderName);

         // Select item based onto operation name.
         if (operationName.equals(operation.getName())) {
            Iterator responses = itemNode.path("response").elements();
            while (responses.hasNext()) {
               JsonNode responseNode = responses.next();
               JsonNode requestNode = responseNode.path("originalRequest");

               // Build dispatchCriteria and complete operation resourcePaths.
               String dispatchCriteria = null;
               String requestUrl = requestNode.path("url").path("raw").asText();

               if (DispatchStyles.URI_PARAMS.equals(operation.getDispatcher())) {
                  dispatchCriteria = DispatchCriteriaHelper.extractFromURIParams(operation.getDispatcherRules(), requestUrl);
               } else if (DispatchStyles.URI_PARTS.equals(operation.getDispatcher())) {
                  Map parts = buildRequestParts(requestNode);
                  dispatchCriteria = DispatchCriteriaHelper.buildFromPartsMap(parts);
                  // We should complete resourcePath here.
                  String resourcePath = extractResourcePath(requestUrl, null);
                  operation.addResourcePath(buildResourcePath(parts, resourcePath));
               } else if (DispatchStyles.URI_ELEMENTS.equals(operation.getDispatcher())) {
                  Map parts = buildRequestParts(requestNode);
                  dispatchCriteria = DispatchCriteriaHelper.buildFromPartsMap(parts);
                  dispatchCriteria += DispatchCriteriaHelper.extractFromURIParams(operation.getDispatcherRules(), requestUrl);
                  // We should complete resourcePath here.
                  String resourcePath = extractResourcePath(requestUrl, null);
                  operation.addResourcePath(buildResourcePath(parts, resourcePath));
               }

               Request request = buildRequest(requestNode, responseNode.path("name").asText());
               Response response = buildResponse(responseNode, dispatchCriteria);
               result.put(request, response);
            }
         }
      }
      return result;
   }

   private Request buildRequest(JsonNode requestNode, String name) {
      Request request = new Request();
      request.setName(name);

      if (isV2Collection) {
         request.setHeaders(buildHeaders(requestNode.path("header")));
         if (requestNode.has("body") && requestNode.path("body").has("raw")) {
            request.setContent(requestNode.path("body").path("raw").asText());
         }
         if (requestNode.path("url").has("variable")) {
            JsonNode variablesNode = requestNode.path("url").path("variable");
            for (JsonNode variableNode : variablesNode) {
               Parameter param = new Parameter();
               param.setName(variableNode.path("key").asText());
               param.setValue(variableNode.path("value").asText());
               request.addQueryParameter(param);
            }
         }
         if (requestNode.path("url").has("query")) {
            JsonNode queryNode = requestNode.path("url").path("query");
            for (JsonNode variableNode : queryNode) {
               Parameter param = new Parameter();
               param.setName(variableNode.path("key").asText());
               param.setValue(variableNode.path("value").asText());
               request.addQueryParameter(param);
            }
         }
      } else {
         request.setHeaders(buildHeaders(requestNode.path("headers")));
      }
      return request;
   }

   private Map buildRequestParts(JsonNode requestNode) {
      Map parts = new HashMap<>();
      if (requestNode.has("url") && requestNode.path("url").has("variable")) {
         Iterator variables = requestNode.path("url").path("variable").elements();
         while (variables.hasNext()) {
            JsonNode variable = variables.next();
            parts.put(variable.path("key").asText(), variable.path("value").asText());
         }
      }
      return parts;
   }

   private Response buildResponse(JsonNode responseNode, String dispatchCriteria) {
      Response response = new Response();
      response.setName(responseNode.path("name").asText());

      if (isV2Collection) {
         response.setStatus(responseNode.path("code").asText("200"));
         response.setHeaders(buildHeaders(responseNode.path("header")));
         response.setContent(responseNode.path("body").asText(""));
      } else {
         response.setStatus(responseNode.path("responseCode").path("code").asText());
         response.setHeaders(buildHeaders(responseNode.path("headers")));
         response.setContent(responseNode.path("text").asText());
      }
      if (response.getHeaders() != null) {
         for (Header header : response.getHeaders()) {
            if (header.getName().equalsIgnoreCase("Content-Type")) {
               response.setMediaType(header.getValues().toArray(new String[]{})[0]);
            }
         }
      }
      // For V1 Collection, if no Content-Type header but response expressed as a language,
      // assume it is its content-type.
      if (!isV2Collection && response.getMediaType() == null) {
         if ("json".equals(responseNode.path("language").asText())) {
            response.setMediaType("application/json");
         }
      }
      // For V2 Collection, if no Content-Type header but response expressed as a language,
      // assume it is its content-type.
      if (isV2Collection && response.getMediaType() == null) {
         if ("json".equals(responseNode.path("_postman_previewlanguage").asText())) {
            response.setMediaType("application/json");
         } else if ("xml".equals(responseNode.path("_postman_previewlanguage").asText())) {
            response.setMediaType("text/xml");
         }
      }
      response.setDispatchCriteria(dispatchCriteria);
      return response;
   }

   private Set
buildHeaders(JsonNode headerNode) { if (headerNode == null || headerNode.size() == 0) { return null; } // Prepare and map the set of headers. Set
headers = new HashSet<>(); Iterator items = headerNode.elements(); while (items.hasNext()) { JsonNode item = items.next(); Header header = new Header(); header.setName(item.path("key").asText()); Set values = new HashSet<>(); values.add(item.path("value").asText()); header.setValues(values); headers.add(header); } return headers; } /** * Extract the list of operations from Collection. */ private List extractOperations() throws MockRepositoryImportException { if (isV2Collection) { return extractOperationsV2(); } throw new MockRepositoryImportException("Only Postman v2 Collection are supported."); } private List extractOperationsV2() { // Items corresponding to same operations may be defined multiple times in Postman // with different names and resource path. We have to track them to complete them in second step. Map collectedOperations = new HashMap(); Iterator items = collection.path("item").elements(); while (items.hasNext()) { JsonNode item = items.next(); extractOperationV2("", item, collectedOperations); } return new ArrayList<>(collectedOperations.values()); } private void extractOperationV2(String folderName, JsonNode itemNode, Map collectedOperations) { log.debug("Extracting operation in folder " + folderName); String itemNodeName = itemNode.path("name").asText(); // Item may be a folder or an operation description. if (!itemNode.has("request")) { // Item is simply a folder that may contain some other folders recursively. Iterator items = itemNode.path("item").elements(); while (items.hasNext()) { JsonNode item = items.next(); extractOperationV2(folderName + "/" + itemNodeName, item, collectedOperations); } } else { // Item is here an operation description. String operationName = buildOperationName(itemNode, folderName); Operation operation = collectedOperations.get(operationName); String url = itemNode.path("request").path("url").asText(""); if ("".equals(url)) { url = itemNode.path("request").path("url").path("raw").asText(""); } // Collection may have been used for testing so it may contain a valid URL with prefix that will bother us. // Ex: http://localhost:8080/prefix1/prefix2/order/123456 or http://petstore.swagger.io/v2/pet/1. Trim it. if (url.indexOf(folderName + "/") != -1) { url = removeProtocolAndHostPort(url); } if (operation == null) { // Build a new operation. operation = new Operation(); operation.setName(operationName); // Complete with REST specific fields. operation.setMethod(itemNode.path("request").path("method").asText()); // Deal with dispatcher stuffs. if (urlHasParameters(url) && urlHasParts(url)) { operation.setDispatcherRules(DispatchCriteriaHelper.extractPartsFromURIPattern(url) + " ?? " + DispatchCriteriaHelper.extractParamsFromURI(url)); operation.setDispatcher(DispatchStyles.URI_ELEMENTS); } else if (urlHasParameters(url)) { operation.setDispatcherRules(DispatchCriteriaHelper.extractParamsFromURI(url)); operation.setDispatcher(DispatchStyles.URI_PARAMS); operation.addResourcePath(extractResourcePath(url, null)); } else if (urlHasParts(url)) { operation.setDispatcherRules(DispatchCriteriaHelper.extractPartsFromURIPattern(url)); operation.setDispatcher(DispatchStyles.URI_PARTS); } else { operation.addResourcePath(extractResourcePath(url, null)); } } // Do not deal with resource path now as it will be done when extracting messages. collectedOperations.put(operationName, operation); } } /** * Build a coherent operation name from the JsonNode of collection representing operation (ie. having a * request item) and an operationNameRadix (ie. a subcontext or nested subcontext folder where operation * is stored). * @param operationNode JSON node for operation * @param folderName String representing radix of operation name * @return Operation name */ public static String buildOperationName(JsonNode operationNode, String folderName) { String url = operationNode.path("request").path("url").asText(""); if ("".equals(url)) { url = operationNode.path("request").path("url").path("raw").asText(); } /* // Old way ot computing operation name. String nameSuffix = url.substring(url.lastIndexOf(operationNameRadix) + operationNameRadix.length()); if (nameSuffix.indexOf('?') != -1) { nameSuffix = nameSuffix.substring(0, nameSuffix.indexOf('?')); } operationNameRadix += nameSuffix; return operationNode.path("request").path("method").asText() + " " + operationNameRadix; */ // New way of computing operation name. if (url.indexOf('?') != - 1) { // Remove query parameters. url = url.substring(0, url.indexOf('?')); } // Remove protocol pragma and host/port stuffs. url = removeProtocolAndHostPort(url); return operationNode.path("request").path("method").asText() + " " + url; } /** * Extract a resource path from a complete url and an optional operationName radix (context or subcontext). * https://petstore-api-2445581593402.apicast.io:443/v2/pet/findByStatus?user_key=998bac0775b1d5f588e0a6ca7c11b852&status=available * => /v2/pet/findByStatus if no operationNameRadix * => /pet/findByStatus if operationNameRadix=/pet */ private String extractResourcePath(String url, String operationNameRadix) { // Remove protocol, host and port specification. String result = removeProtocolAndHostPort(url); // Remove trailing parameters. if (result.indexOf('?') != -1) { result = result.substring(0, result.indexOf('?')); } // Remove prefix of radix if specified. if (operationNameRadix != null && result.indexOf(operationNameRadix) != -1) { result = result.substring(result.indexOf(operationNameRadix)); } return result; } /** * Build a resource path a Map for parts and a patter url * <(id:123)> && /order/:id => /order/123 */ private String buildResourcePath(Map parts, String patternUrl) { StringBuilder result = new StringBuilder(); for (String token : patternUrl.split("/")) { if (token.startsWith(":") && token.length() > 1) { result.append("/").append(parts.get(token.substring(1))); } else if (token.length() > 0) { result.append("/").append(token); } } return result.toString(); } /** Check parameters presence into given url. */ private static boolean urlHasParameters(String url) { return url.indexOf('?') != -1 && url.indexOf('=') != -1; } /** Check variables parts presence into given url. */ private static boolean urlHasParts(String url) { return url.indexOf("/:") != -1; } private static String removeProtocolAndHostPort(String url) { if (url.startsWith("https://")) { url = url.substring("https://".length()); } if (url.startsWith("http://")) { url = url.substring("http://".length()); } // Remove host and port specification if any. if (url.indexOf('/') != -1) { url = url.substring(url.indexOf('/')); } return url; } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy