Merge "Release 1.14.0 maven artifact" master
authorNandkishor Patke <nandkishor-laxman.patke@t-systems.com>
Wed, 5 Jun 2024 03:02:14 +0000 (03:02 +0000)
committerGerrit Code Review <gerrit@onap.org>
Wed, 5 Jun 2024 03:02:14 +0000 (03:02 +0000)
35 files changed:
aai-aaf-auth/pom.xml
aai-annotations/pom.xml
aai-auth/pom.xml
aai-common-docker/aai-common-images/pom.xml
aai-common-docker/aai-haproxy-image/pom.xml
aai-common-docker/pom.xml
aai-core/pom.xml
aai-core/src/main/java/org/onap/aai/dbgen/GraphSONPartialIO.java
aai-core/src/main/java/org/onap/aai/dbmap/AAIGraph.java
aai-core/src/main/java/org/onap/aai/prevalidation/ValidationService.java
aai-core/src/main/java/org/onap/aai/query/builder/GraphTraversalBuilder.java
aai-core/src/main/java/org/onap/aai/query/builder/GremlinQueryBuilder.java
aai-core/src/main/java/org/onap/aai/query/builder/QueryBuilder.java
aai-core/src/main/java/org/onap/aai/rest/db/HttpEntry.java
aai-core/src/main/java/org/onap/aai/rest/ueb/UEBNotification.java
aai-core/src/main/java/org/onap/aai/restcore/search/AAIAbstractGroovyShell.java
aai-core/src/main/java/org/onap/aai/serialization/queryformats/GraphSON.java
aai-core/src/test/java/org/onap/aai/AAISetup.java
aai-core/src/test/java/org/onap/aai/TinkerpopUpgradeTests.java
aai-core/src/test/java/org/onap/aai/db/DbMethHelperTest.java
aai-core/src/test/java/org/onap/aai/rest/db/HttpEntryTest.java
aai-els-onap-logging/pom.xml
aai-els-onap-logging/src/main/java/org/onap/aai/logging/ErrorLogHelper.java
aai-els-onap-logging/src/test/java/org/onap/aai/logging/ErrorLogHelperTest.java
aai-failover/pom.xml
aai-parent/pom.xml
aai-rest/pom.xml
aai-schema-abstraction/pom.xml
aai-schema-ingest/pom.xml
aai-schema-ingest/src/main/java/org/onap/aai/nodes/NodeIngestor.java
aai-utils/pom.xml
pom.xml
releases/1.13.6-container-release.yaml [new file with mode: 0644]
releases/1.14.0-maven-release.yaml [new file with mode: 0644]
version.properties

index 0fa1e8c..f0ef4a9 100644 (file)
@@ -6,7 +6,7 @@
     <parent>
         <groupId>org.onap.aai.aai-common</groupId>
         <artifactId>aai-parent</artifactId>
-        <version>1.13.6-SNAPSHOT</version>
+        <version>1.14.0-SNAPSHOT</version>
         <relativePath>../aai-parent/pom.xml</relativePath>
     </parent>
     <artifactId>aai-aaf-auth</artifactId>
index cae569d..7f92056 100644 (file)
@@ -27,7 +27,7 @@
     <parent>
         <groupId>org.onap.aai.aai-common</groupId>
         <artifactId>aai-parent</artifactId>
-        <version>1.13.6-SNAPSHOT</version>
+        <version>1.14.0-SNAPSHOT</version>
         <relativePath>../aai-parent/pom.xml</relativePath>
     </parent>
     <artifactId>aai-annotations</artifactId>
index 7ff240b..5de5805 100644 (file)
@@ -27,7 +27,7 @@
     <parent>
         <groupId>org.onap.aai.aai-common</groupId>
         <artifactId>aai-parent</artifactId>
-        <version>1.13.6-SNAPSHOT</version>
+        <version>1.14.0-SNAPSHOT</version>
         <relativePath>../aai-parent/pom.xml</relativePath>
     </parent>
     <artifactId>aai-auth</artifactId>
index 1695edb..a4d4cd7 100644 (file)
     <parent>
         <groupId>org.onap.aai.aai-common</groupId>
         <artifactId>aai-common-docker</artifactId>
-        <version>1.13.6-SNAPSHOT</version>
+        <version>1.14.0-SNAPSHOT</version>
     </parent>
 
     <artifactId>aai-common-images</artifactId>
-    <version>1.13.6-SNAPSHOT</version>
+    <version>1.14.0-SNAPSHOT</version>
     <packaging>pom</packaging>
     <name>aai-aai-common-images</name>
     <description>Contains dockerfiles for aai-common images (alpine and ubuntu based).</description>
index 5f43948..df458ad 100644 (file)
     <parent>
         <groupId>org.onap.aai.aai-common</groupId>
         <artifactId>aai-common-docker</artifactId>
-        <version>1.13.6-SNAPSHOT</version>
+        <version>1.14.0-SNAPSHOT</version>
     </parent>
 
     <artifactId>aai-haproxy-image</artifactId>
-    <version>1.13.6-SNAPSHOT</version>
+    <version>1.14.0-SNAPSHOT</version>
     <packaging>pom</packaging>
     <name>aai-aai-haproxy-image</name>
     <description>Contains dockerfiles for aai-haproxy image.</description>
index 5c0298b..bdf6721 100644 (file)
@@ -26,7 +26,7 @@
     <parent>
         <groupId>org.onap.aai.aai-common</groupId>
         <artifactId>aai-parent</artifactId>
-        <version>1.13.6-SNAPSHOT</version>
+        <version>1.14.0-SNAPSHOT</version>
         <relativePath>../aai-parent/pom.xml</relativePath>
     </parent>
 
index b9814e8..86ae097 100644 (file)
@@ -26,7 +26,7 @@ limitations under the License.
        <parent>
                <groupId>org.onap.aai.aai-common</groupId>
                <artifactId>aai-parent</artifactId>
-               <version>1.13.6-SNAPSHOT</version>
+               <version>1.14.0-SNAPSHOT</version>
                <relativePath>../aai-parent/pom.xml</relativePath>
        </parent>
        <artifactId>aai-core</artifactId>
@@ -365,6 +365,12 @@ limitations under the License.
                        <groupId>org.skyscreamer</groupId>
                        <artifactId>jsonassert</artifactId>
                        <scope>test</scope>
+                       <exclusions>
+                               <exclusion>
+                                       <groupId>com.vaadin.external.google</groupId>
+                                       <artifactId>android-json</artifactId>
+                               </exclusion>
+                       </exclusions>
                </dependency>
                <dependency>
                        <groupId>org.apache.httpcomponents</groupId>
@@ -396,6 +402,10 @@ limitations under the License.
                        <groupId>com.fasterxml.jackson.core</groupId>
                        <artifactId>jackson-annotations</artifactId>
                </dependency>
+               <dependency>
+                       <groupId>com.fasterxml.jackson.dataformat</groupId>
+                       <artifactId>jackson-dataformat-xml</artifactId>
+               </dependency>
                <dependency>
                        <groupId>com.fasterxml.jackson.dataformat</groupId>
                        <artifactId>jackson-dataformat-yaml</artifactId>
index 6c2eee1..c097a5b 100644 (file)
@@ -129,16 +129,6 @@ public final class GraphSONPartialIO
             this.version = version;
         }
 
-        /**
-         * @deprecated As of release 3.2.2, replaced by {@link #onMapper(Consumer)}.
-         */
-        @Deprecated
-        @Override
-        public Io.Builder<GraphSONPartialIO> registry(final IoRegistry registry) {
-            this.registry = registry;
-            return this;
-        }
-
         @Override
         public Io.Builder<? extends Io> onMapper(final Consumer<Mapper.Builder> onMapper) {
             this.onMapper = onMapper;
index d9994f5..15f57c1 100644 (file)
@@ -70,6 +70,7 @@ public class AAIGraph {
             }
             this.loadGraph(rtConfig, serviceName);
         } catch (Exception e) {
+            logger.error("Failed to instantiate graph", e);
             throw new RuntimeException("Failed to instantiate graphs", e);
         }
     }
index 70e16e2..939c838 100644 (file)
@@ -29,6 +29,7 @@ import java.net.ConnectException;
 import java.net.SocketTimeoutException;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -46,7 +47,6 @@ import org.onap.aai.rest.ueb.NotificationEvent;
 import org.onap.aai.restclient.RestClient;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
-import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Qualifier;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.context.annotation.Profile;
@@ -63,55 +63,33 @@ import org.springframework.stereotype.Service;
 @Profile("pre-validation")
 public class ValidationService {
 
-    /**
-     * Error indicating that the service trying to connect is down
-     */
     static final String CONNECTION_REFUSED_STRING =
             "Connection refused to the validation microservice due to service unreachable";
-
-    /**
-     * Error indicating that the server is unable to reach the port
-     * Could be server related connectivity issue
-     */
     static final String CONNECTION_TIMEOUT_STRING = "Connection timeout to the validation microservice as this could "
             + "indicate the server is unable to reach port, "
             + "please check on server by running: nc -w10 -z -v ${VALIDATION_HOST} ${VALIDATION_PORT}";
-
-    /**
-     * Error indicating that the request exceeded the allowed time
-     *
-     * Note: This means that the service could be active its
-     * just taking some time to process our request
-     */
     static final String REQUEST_TIMEOUT_STRING =
             "Request to validation service took longer than the currently set timeout";
-
     static final String VALIDATION_ENDPOINT = "/v1/validate";
     static final String VALIDATION_HEALTH_ENDPOINT = "/v1/info";
 
+    private static final Logger LOGGER = LoggerFactory.getLogger(ValidationService.class);
     private static final String ENTITY_TYPE = "entity-type";
     private static final String ACTION = "action";
     private static final String SOURCE_NAME = "source-name";
-
     private static final String DELETE = "DELETE";
 
-    private static final Logger LOGGER = LoggerFactory.getLogger(ValidationService.class);
-
     private final RestClient validationRestClient;
-
     private final String appName;
-
     private final Set<String> validationNodeTypes;
+    private final Gson gson;
 
     private List<Pattern> exclusionList;
 
-    private final Gson gson;
-
-    @Autowired
     public ValidationService(@Qualifier("validationRestClient") RestClient validationRestClient,
             @Value("${spring.application.name}") String appName,
             @Value("${validation.service.node-types}") String validationNodes,
-            @Value("${validation.service.exclusion-regexes}") String exclusionRegexes) {
+            @Value("${validation.service.exclusion-regexes:#{null}}") String exclusionRegexes) {
         this.validationRestClient = validationRestClient;
         this.appName = appName;
 
@@ -129,20 +107,19 @@ public class ValidationService {
 
     @PostConstruct
     public void initialize() throws AAIException {
+        doHealthCheckRequest();
+    }
 
+    private void doHealthCheckRequest() throws AAIException {
         Map<String, String> httpHeaders = new HashMap<>();
-
         httpHeaders.put("X-FromAppId", appName);
         httpHeaders.put("X-TransactionID", UUID.randomUUID().toString());
         httpHeaders.put("Content-Type", "application/json");
 
         ResponseEntity<String> healthCheckResponse = null;
-
         try {
-
             healthCheckResponse =
                     validationRestClient.execute(VALIDATION_HEALTH_ENDPOINT, HttpMethod.GET, httpHeaders, null);
-
         } catch (Exception ex) {
             AAIException validationException = new AAIException("AAI_4021", ex);
             throw validationException;
@@ -160,50 +137,29 @@ public class ValidationService {
     }
 
     public void validate(List<NotificationEvent> notificationEvents) throws AAIException {
-
-        if (notificationEvents == null || notificationEvents.isEmpty()) {
+        if (notificationEvents == null || notificationEvents.isEmpty() || isSourceExcluded(notificationEvents)) {
             return;
         }
 
-        {
-            // Get the first notification and if the source of that notification
-            // is in one of the regexes then we skip sending it to validation
-            NotificationEvent notification = notificationEvents.get(0);
-            Introspector eventHeader = notification.getEventHeader();
-            if (eventHeader != null) {
-                String source = eventHeader.getValue(SOURCE_NAME);
-                for (Pattern pattern : exclusionList) {
-                    if (pattern.matcher(source).matches()) {
-                        return;
-                    }
-                }
-            }
-
-        }
-
         for (NotificationEvent event : notificationEvents) {
-
             Introspector eventHeader = event.getEventHeader();
-
             if (eventHeader == null) {
                 // Should I skip processing the request and let it continue
                 // or fail the request and cause client impact
                 continue;
             }
 
-            String entityType = eventHeader.getValue(ENTITY_TYPE);
-            String action = eventHeader.getValue(ACTION);
-
-            /**
+            /*
              * Skipping the delete events for now
              * Note: Might revisit this later when validation supports DELETE events
              */
-            if (DELETE.equalsIgnoreCase(action)) {
+            if (isDelete(eventHeader)) {
                 continue;
             }
+            String entityType = eventHeader.getValue(ENTITY_TYPE);
 
             if (this.shouldValidate(entityType)) {
-                List<String> violations = this.preValidate(event.getNotificationEvent());
+                List<String> violations = preValidate(event.getNotificationEvent());
                 if (!violations.isEmpty()) {
                     AAIException aaiException = new AAIException("AAI_4019");
                     aaiException.getTemplateVars().addAll(violations);
@@ -213,10 +169,33 @@ public class ValidationService {
         }
     }
 
-    List<String> preValidate(String body) throws AAIException {
+    /**
+     * Determine if event is of type delete
+     */
+    private boolean isDelete(Introspector eventHeader) {
+        String action = eventHeader.getValue(ACTION);
+        return DELETE.equalsIgnoreCase(action);
+    }
+
+    /**
+     * Checks the `source` attribute of the first event to determine if validation should be skipped
+     * @param notificationEvents
+     * @return
+     */
+    private boolean isSourceExcluded(List<NotificationEvent> notificationEvents) {
+        // Get the first notification and if the source of that notification
+        // is in one of the regexes then we skip sending it to validation
+        NotificationEvent notification = notificationEvents.get(0);
+        Introspector eventHeader = notification.getEventHeader();
+        if (eventHeader != null) {
+            String source = eventHeader.getValue(SOURCE_NAME);
+            return exclusionList.stream().anyMatch(pattern -> pattern.matcher(source).matches());
+        }
+        return false;
+    }
 
+    public List<String> preValidate(String body) throws AAIException {
         Map<String, String> httpHeaders = new HashMap<>();
-
         httpHeaders.put("X-FromAppId", appName);
         httpHeaders.put("X-TransactionID", UUID.randomUUID().toString());
         httpHeaders.put("Content-Type", "application/json");
@@ -224,26 +203,19 @@ public class ValidationService {
         List<String> violations = new ArrayList<>();
         ResponseEntity<String> responseEntity;
         try {
-
             responseEntity = validationRestClient.execute(VALIDATION_ENDPOINT, HttpMethod.POST, httpHeaders, body);
-
             Object responseBody = responseEntity.getBody();
             if (isSuccess(responseEntity)) {
                 LOGGER.debug("Validation Service returned following response status code {} and body {}",
                         responseEntity.getStatusCodeValue(), responseEntity.getBody());
             } else if (responseBody != null) {
-                Validation validation = null;
-                try {
-                    validation = gson.fromJson(responseBody.toString(), Validation.class);
-                } catch (JsonSyntaxException jsonException) {
-                    LOGGER.warn("Unable to convert the response body {}", jsonException.getMessage());
-                }
+                Validation validation = getValidation(responseBody);
 
                 if (validation == null) {
                     LOGGER.debug("Validation Service following status code {} with body {}",
                             responseEntity.getStatusCodeValue(), responseEntity.getBody());
                 } else {
-                    violations.addAll(extractViolations(validation));
+                    violations = extractViolations(validation);
                 }
             } else {
                 LOGGER.warn("Unable to convert the response body null");
@@ -267,27 +239,27 @@ public class ValidationService {
         return violations;
     }
 
+    private Validation getValidation(Object responseBody) {
+        Validation validation = null;
+        try {
+            validation = gson.fromJson(responseBody.toString(), Validation.class);
+        } catch (JsonSyntaxException jsonException) {
+            LOGGER.warn("Unable to convert the response body {}", jsonException.getMessage());
+        }
+        return validation;
+    }
+
     boolean isSuccess(ResponseEntity<String> responseEntity) {
         return responseEntity != null && responseEntity.getStatusCode().is2xxSuccessful();
     }
 
-    List<String> extractViolations(Validation validation) {
-
-        List<String> errorMessages = new ArrayList<>();
-
-        if (validation == null) {
-            return errorMessages;
+    public List<String> extractViolations(Validation validation) {
+        if (validation == null || validation.getViolations() == null) {
+            return Collections.emptyList();
         }
-
-        List<Violation> violations = validation.getViolations();
-
-        if (violations != null && !violations.isEmpty()) {
-            for (Violation violation : validation.getViolations()) {
-                LOGGER.info(violation.getErrorMessage());
-                errorMessages.add(violation.getErrorMessage());
-            }
-        }
-
-        return errorMessages;
+        return validation.getViolations().stream()
+            .map(Violation::getErrorMessage)
+            .peek(LOGGER::info)
+            .collect(Collectors.toList());
     }
 }
index c5c4512..24e5ec8 100644 (file)
@@ -34,7 +34,7 @@ import java.util.Set;
 
 import org.apache.tinkerpop.gremlin.process.traversal.P;
 import org.apache.tinkerpop.gremlin.process.traversal.Path;
-import org.apache.tinkerpop.gremlin.process.traversal.Step;
+import org.apache.tinkerpop.gremlin.process.traversal.Pop;
 import org.apache.tinkerpop.gremlin.process.traversal.Traversal;
 import org.apache.tinkerpop.gremlin.process.traversal.Traversal.Admin;
 import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversal;
@@ -688,6 +688,15 @@ public abstract class GraphTraversalBuilder<E> extends QueryBuilder<E> {
         return this;
     }
 
+    @Override
+    public QueryBuilder<E> select(Pop pop, String name) {
+        this.traversal.select(pop, name);
+
+        stepIndex++;
+
+        return this;
+    }
+
     @Override
     public QueryBuilder<E> select(String... names) {
         if (names.length == 1) {
index 4500a47..db1c78a 100644 (file)
@@ -29,6 +29,7 @@ import com.google.common.collect.Multimap;
 import java.util.*;
 
 import org.apache.tinkerpop.gremlin.process.traversal.Path;
+import org.apache.tinkerpop.gremlin.process.traversal.Pop;
 import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversal;
 import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversalSource;
 import org.apache.tinkerpop.gremlin.process.traversal.step.util.Tree;
@@ -827,6 +828,14 @@ public abstract class GremlinQueryBuilder<E> extends QueryBuilder<E> {
         return this;
     }
 
+    @Override
+    public QueryBuilder<E> select(Pop pop, String name) {
+        this.list.add(".select(Pop." + pop.toString() + ",'" + name + "')");
+        stepIndex++;
+
+        return this;
+    }
+
     @Override
     public QueryBuilder<E> select(String... names) {
         String stepString = ".select('";
index 6653252..309ffa1 100644 (file)
@@ -31,6 +31,7 @@ import java.util.Map;
 import javax.ws.rs.core.MultivaluedMap;
 
 import org.apache.tinkerpop.gremlin.process.traversal.Path;
+import org.apache.tinkerpop.gremlin.process.traversal.Pop;
 import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversalSource;
 import org.apache.tinkerpop.gremlin.process.traversal.step.util.Tree;
 import org.apache.tinkerpop.gremlin.structure.Edge;
@@ -704,6 +705,8 @@ public abstract class QueryBuilder<E> implements Iterator<E> {
 
     public abstract QueryBuilder<E> select(String name);
 
+    public abstract QueryBuilder<E> select(Pop pop, String name);
+
     public abstract QueryBuilder<E> select(String... names);
 
     public abstract QueryBuilder<E> until(QueryBuilder<E> builder);
index 28d66dd..7ecdd6d 100644 (file)
@@ -155,49 +155,22 @@ public class HttpEntry {
     }
 
     public HttpEntry setHttpEntryProperties(SchemaVersion version, String serverBase) {
-        this.version = version;
-        this.loader = loaderFactory.createLoaderForVersion(introspectorFactoryType, version);
-        this.dbEngine = new JanusGraphDBEngine(queryStyle, loader);
-
-        getDbEngine().startTransaction();
-        this.notification = new UEBNotification(loader, loaderFactory, schemaVersions);
-        if ("true".equals(AAIConfig.get("aai.notification.depth.all.enabled", "true"))) {
-            this.notificationDepth = AAIProperties.MAXIMUM_DEPTH;
-        } else {
-            this.notificationDepth = AAIProperties.MINIMUM_DEPTH;
-        }
-
+        setHttpEntryProperties(version);
         this.serverBase = serverBase;
         return this;
     }
 
     public HttpEntry setHttpEntryProperties(SchemaVersion version, UEBNotification notification) {
-        this.version = version;
-        this.loader = loaderFactory.createLoaderForVersion(introspectorFactoryType, version);
-        this.dbEngine = new JanusGraphDBEngine(queryStyle, loader);
-
+        setHttpEntryProperties(version);
         this.notification = notification;
-
-        if ("true".equals(AAIConfig.get("aai.notification.depth.all.enabled", "true"))) {
-            this.notificationDepth = AAIProperties.MAXIMUM_DEPTH;
-        } else {
-            this.notificationDepth = AAIProperties.MINIMUM_DEPTH;
-        }
-        // start transaction on creation
-        getDbEngine().startTransaction();
         return this;
     }
 
     public HttpEntry setHttpEntryProperties(SchemaVersion version, UEBNotification notification,
             int notificationDepth) {
-        this.version = version;
-        this.loader = loaderFactory.createLoaderForVersion(introspectorFactoryType, version);
-        this.dbEngine = new JanusGraphDBEngine(queryStyle, loader);
-
+        setHttpEntryProperties(version);
         this.notification = notification;
         this.notificationDepth = notificationDepth;
-        // start transaction on creation
-        getDbEngine().startTransaction();
         return this;
     }
 
index be30c46..0c8fde6 100644 (file)
@@ -98,42 +98,15 @@ public class UEBNotification {
             Introspector obj, HashMap<String, Introspector> relatedObjects, String basePath)
             throws AAIException, UnsupportedEncodingException {
 
-        String action = "UPDATE";
-
-        if (status.equals(Status.CREATED)) {
-            action = "CREATE";
-        } else if (status.equals(Status.OK)) {
-            action = "UPDATE";
-        } else if (status.equals(Status.NO_CONTENT)) {
-            action = "DELETE";
-        }
+        String action = getAction(status);
 
         try {
             Introspector eventHeader = currentVersionLoader.introspectorFromName("notification-event-header");
             URIToObject parser = new URIToObject(currentVersionLoader, uri, relatedObjects);
 
-            if ((basePath != null) && (!basePath.isEmpty())) {
-                if (!(basePath.startsWith("/"))) {
-                    basePath = "/" + basePath;
-                }
-                if (!(basePath.endsWith("/"))) {
-                    basePath = basePath + "/";
-                }
-            } else {
-                // default
-                basePath = "/aai/";
-                if (LOGGER.isDebugEnabled()) {
-                    LOGGER.debug("Please check the schema.uri.base.path as it didn't seem to be set");
-                }
-            }
+            basePath = formatBasePath(basePath);
 
-            String uriStr = getUri(uri.toString(), basePath);
-            String entityLink;
-            if (uriStr.startsWith("/")) {
-                entityLink = basePath + notificationVersion + uriStr;
-            } else {
-                entityLink = basePath + notificationVersion + "/" + uriStr;
-            }
+            String entityLink = formatEntityLink(uri, basePath);
 
             eventHeader.setValue("entity-link", entityLink);
             eventHeader.setValue("action", action);
@@ -191,6 +164,48 @@ public class UEBNotification {
         }
     }
 
+    private String formatEntityLink(URI uri, String basePath) {
+        String uriStr = getUri(uri.toString(), basePath);
+        String entityLink;
+        if (uriStr.startsWith("/")) {
+            entityLink = basePath + notificationVersion + uriStr;
+        } else {
+            entityLink = basePath + notificationVersion + "/" + uriStr;
+        }
+        return entityLink;
+    }
+
+    private String formatBasePath(String basePath) {
+        if ((basePath != null) && (!basePath.isEmpty())) {
+            if (!(basePath.startsWith("/"))) {
+                basePath = "/" + basePath;
+            }
+            if (!(basePath.endsWith("/"))) {
+                basePath = basePath + "/";
+            }
+        } else {
+            // default
+            basePath = "/aai/";
+            if (LOGGER.isDebugEnabled()) {
+                LOGGER.debug("Please check the schema.uri.base.path as it didn't seem to be set");
+            }
+        }
+        return basePath;
+    }
+
+    private String getAction(Status status) {
+        String action = "UPDATE";
+
+        if (status.equals(Status.CREATED)) {
+            action = "CREATE";
+        } else if (status.equals(Status.OK)) {
+            action = "UPDATE";
+        } else if (status.equals(Status.NO_CONTENT)) {
+            action = "DELETE";
+        }
+        return action;
+    }
+
     /**
      * Trigger events.
      *
index 4a8760e..f63df4b 100644 (file)
@@ -59,7 +59,7 @@ public abstract class AAIAbstractGroovyShell {
                 "org.apache.tinkerpop.gremlin.process.traversal.Order");
         imports.addImports("org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.__",
                 "org.apache.tinkerpop.gremlin.structure.T", "org.apache.tinkerpop.gremlin.process.traversal.P",
-                "org.onap.aai.edges.enums.EdgeType", "java.util.Map.Entry");
+                "org.onap.aai.edges.enums.EdgeType", "java.util.Map.Entry","org.apache.tinkerpop.gremlin.process.traversal.Pop");
         imports.addStarImports("java.util");
         CompilerConfiguration config = new CompilerConfiguration();
         config.addCompilationCustomizers(custom, imports);
index c0ba860..89970e2 100644 (file)
@@ -36,6 +36,7 @@ import java.util.Optional;
 import org.apache.tinkerpop.gremlin.structure.Direction;
 import org.apache.tinkerpop.gremlin.structure.Vertex;
 import org.apache.tinkerpop.gremlin.structure.io.graphson.GraphSONMapper;
+import org.apache.tinkerpop.gremlin.structure.io.graphson.GraphSONVersion;
 import org.apache.tinkerpop.gremlin.structure.io.graphson.GraphSONWriter;
 import org.janusgraph.graphdb.tinkerpop.JanusGraphIoRegistry;
 import org.onap.aai.serialization.queryformats.exceptions.AAIFormatQueryResultFormatNotSupported;
@@ -47,8 +48,13 @@ public class GraphSON implements FormatMapper {
 
     private static final Logger logger = LoggerFactory.getLogger(GraphSON.class);
 
+    // TODO: Use v2 or v3 here
+    // v2 is the default starting from Janusgraph 0.3.0
+    // Further reference: https://tinkerpop.apache.org/docs/3.3.0/dev/io/#graphson
+    private static final GraphSONVersion version = GraphSONVersion.V1_0;
+
     private final GraphSONMapper mapper =
-            GraphSONMapper.build().addRegistry(JanusGraphIoRegistry.getInstance()).create();
+            GraphSONMapper.build().version(version).addRegistry(JanusGraphIoRegistry.getInstance()).create();
     private final GraphSONWriter writer = GraphSONWriter.build().mapper(mapper).create();
 
     @Override
index a44226c..e1fc351 100644 (file)
@@ -30,6 +30,8 @@ import org.onap.aai.edges.EdgeIngestor;
 import org.onap.aai.introspection.LoaderFactory;
 import org.onap.aai.introspection.MoxyLoader;
 import org.onap.aai.nodes.NodeIngestor;
+import org.onap.aai.prevalidation.ValidationConfiguration;
+import org.onap.aai.prevalidation.ValidationService;
 import org.onap.aai.rest.db.HttpEntry;
 import org.onap.aai.serialization.db.EdgeSerializer;
 import org.onap.aai.serialization.queryformats.QueryFormatTestHelper;
@@ -49,7 +51,7 @@ import org.springframework.test.context.web.WebAppConfiguration;
 @ContextConfiguration(
         classes = {ConfigConfiguration.class, AAIConfigTranslator.class, EdgeIngestor.class, EdgeSerializer.class,
                 NodeIngestor.class, SpringContextAware.class, IntrospectionConfig.class, RestBeanConfig.class,
-                XmlFormatTransformerConfiguration.class})
+                XmlFormatTransformerConfiguration.class, ValidationService.class, ValidationConfiguration.class})
 @TestPropertySource(
         properties = {"schema.uri.base.path = /aai", "schema.xsd.maxoccurs = 5000", "schema.translator.list=config",
                 "schema.nodes.location=src/test/resources/onap/oxm",
index 799f606..f443b30 100644 (file)
@@ -24,6 +24,7 @@ import org.junit.experimental.categories.Categories;
 import org.junit.experimental.categories.Categories.IncludeCategory;
 import org.junit.runner.RunWith;
 import org.junit.runners.Suite.SuiteClasses;
+import org.onap.aai.db.DbMethHelperTest;
 import org.onap.aai.introspection.sideeffect.DataLinkTest;
 import org.onap.aai.parsers.query.GraphTraversalTest;
 import org.onap.aai.query.builder.TraversalQueryTest;
@@ -45,7 +46,8 @@ import org.onap.aai.query.builder.TraversalURIOptimizedQueryTest;
     DataLinkTest.class,
     GraphTraversalTest.class,
     TraversalQueryTest.class,
-    TraversalURIOptimizedQueryTest.class
+    TraversalURIOptimizedQueryTest.class,
+    DbMethHelperTest.class
 })
 public class TinkerpopUpgradeTests {
 }
index 16faa5f..c79b914 100644 (file)
@@ -36,6 +36,7 @@ import org.apache.commons.io.IOUtils;
 import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversalSource;
 import org.apache.tinkerpop.gremlin.structure.Graph;
 import org.apache.tinkerpop.gremlin.structure.Vertex;
+import org.apache.tinkerpop.gremlin.structure.VertexProperty;
 import org.janusgraph.core.JanusGraphFactory;
 import org.junit.After;
 import org.junit.AfterClass;
@@ -44,10 +45,12 @@ import org.junit.Before;
 import org.junit.BeforeClass;
 import org.junit.Rule;
 import org.junit.Test;
+import org.junit.experimental.categories.Category;
 import org.junit.rules.ExpectedException;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
 import org.onap.aai.AAISetup;
+import org.onap.aai.TinkerpopUpgrade;
 import org.onap.aai.exceptions.AAIException;
 import org.onap.aai.introspection.Introspector;
 import org.onap.aai.introspection.Loader;
@@ -62,6 +65,7 @@ import org.onap.aai.setup.SchemaVersion;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.test.annotation.DirtiesContext;
 
+@Category(TinkerpopUpgrade.class)
 @RunWith(value = Parameterized.class)
 @DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_CLASS)
 public class DbMethHelperTest extends AAISetup {
@@ -127,12 +131,10 @@ public class DbMethHelperTest extends AAISetup {
         map.put("pserver.hostname", "testSearchVertexByIdentityMap-pserver-hostname-01");
 
         Optional<Vertex> optionalVertex;
-        try {
-            optionalVertex = dbMethHelper.searchVertexByIdentityMap(type, map);
-        } catch (Exception e) {
-            throw new Exception(e);
-        }
-        Assert.assertEquals("vp[hostname->testSearchVertexById]", optionalVertex.get().property("hostname").toString());
+        optionalVertex = dbMethHelper.searchVertexByIdentityMap(type, map);
+
+        String hostname = (String) optionalVertex.get().property("hostname").value();
+        Assert.assertEquals("testSearchVertexByIdentityMap-pserver-hostname-01", hostname);
     }
 
     @Test(expected = AmbiguousMapAAIException.class)
index 371c07a..59a0f1f 100644 (file)
@@ -31,6 +31,8 @@ import static org.junit.Assert.assertThat;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 import com.fasterxml.jackson.core.JsonProcessingException;
@@ -82,6 +84,7 @@ import org.onap.aai.introspection.Introspector;
 import org.onap.aai.introspection.Loader;
 import org.onap.aai.introspection.ModelType;
 import org.onap.aai.parsers.query.QueryParser;
+import org.onap.aai.prevalidation.ValidationService;
 import org.onap.aai.rest.db.responses.ErrorResponse;
 import org.onap.aai.rest.db.responses.Relationship;
 import org.onap.aai.rest.db.responses.RelationshipWrapper;
@@ -93,11 +96,14 @@ import org.onap.aai.serialization.engines.TransactionalGraphEngine;
 import org.onap.aai.util.AAIConfig;
 import org.skyscreamer.jsonassert.JSONAssert;
 import org.skyscreamer.jsonassert.JSONCompareMode;
+import org.springframework.boot.test.mock.mockito.MockBean;
 
 @RunWith(value = Parameterized.class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
 public class HttpEntryTest extends AAISetup {
 
+    @MockBean ValidationService validationService;
+
     protected static final MediaType APPLICATION_JSON = MediaType.valueOf("application/json");
 
     private static final Set<Integer> VALID_HTTP_STATUS_CODES = new HashSet<>();
@@ -116,7 +122,7 @@ public class HttpEntryTest extends AAISetup {
      */
     @Parameterized.Parameters(name = "QueryStyle.{0}")
     public static Collection<Object[]> data() {
-        return Arrays.asList(new Object[][] { { QueryStyle.TRAVERSAL }, { QueryStyle.TRAVERSAL_URI } });
+        return Arrays.asList(new Object[][] { { QueryStyle.TRAVERSAL } });
     }
 
     private Loader loader;
@@ -196,8 +202,15 @@ public class HttpEntryTest extends AAISetup {
                 .put("equip-type", "theEquipType")
                 .toString();
 
+        JSONObject expectedResponseBody = new JSONObject()
+                .put("hostname", "theHostname")
+                .put("equip-type", "theEquipType");
         Response response = doRequest(traversalHttpEntry, loader, dbEngine, HttpMethod.GET, uri, requestBody);
+        JSONObject actualResponseBody = new JSONObject(response.getEntity().toString());
+
+        JSONAssert.assertEquals(expectedResponseBody, actualResponseBody, JSONCompareMode.NON_EXTENSIBLE);
         assertEquals("Expected the pserver to be returned", 200, response.getStatus());
+        verify(validationService, times(1)).validate(any());
     }
 
     @Test
@@ -216,6 +229,7 @@ public class HttpEntryTest extends AAISetup {
 
         Response response = doRequest(traversalHttpEntry, loader, dbEngine, HttpMethod.PUT, uri, requestBody);
         assertEquals("Expecting the pserver to be created", 201, response.getStatus());
+        verify(validationService, times(1)).validate(any());
     }
 
     @Test
@@ -254,6 +268,7 @@ public class HttpEntryTest extends AAISetup {
         assertEquals("Expecting the pserver to be updated", 200, response.getStatus());
         assertTrue("That old properties are removed",
                 traversal.V().has("hostname", "updatedHostname").hasNot("number-of-cpus").hasNext());
+        verify(validationService, times(1)).validate(any());
     }
 
     @Test
@@ -322,6 +337,7 @@ public class HttpEntryTest extends AAISetup {
                 traversal.V().has("aai-node-type", "p-interface").has("aai-uri", uri).has("interface-name", "p1")
                         .out("tosca.relationships.network.BindsTo").has("aai-node-type", "pserver")
                         .has("hostname", "hostname").hasNext());
+        verify(validationService, times(1)).validate(any());
     }
 
     @Test
@@ -341,6 +357,7 @@ public class HttpEntryTest extends AAISetup {
         assertTrue("object should be updated while keeping old properties",
                 traversal.V().has("aai-node-type", "pserver").has("hostname", "new-hostname")
                         .has("equip-type", "the-equip-type").hasNext());
+        verify(validationService, times(1)).validate(any());
     }
 
     @Test
@@ -357,6 +374,7 @@ public class HttpEntryTest extends AAISetup {
                 doDelete(resourceVersion, uri, "pserver").getStatus());
         assertTrue("Expecting the pserver to be deleted",
                 !traversal.V().has("aai-node-type", "pserver").has("hostname", "the-hostname").hasNext());
+        verify(validationService, times(1)).validate(any());
     }
 
     @Test
@@ -406,6 +424,7 @@ public class HttpEntryTest extends AAISetup {
                 .has(EdgeProperty.PREVENT_DELETE.toString(), "IN");
         assertTrue("p-server has incoming edge from complex", vertexQuery.hasNext());
         assertTrue("Created Edge has expected properties", edgeQuery.hasNext());
+        verify(validationService, times(1)).validate(any());
     }
 
     @Test
@@ -524,6 +543,7 @@ public class HttpEntryTest extends AAISetup {
         assertEquals("Expected get to succeed", 200, response.getStatus());
         assertThat(responseEntity, containsString("/cloud-infrastructure/pservers/pserver/pserver-1"));
         assertThat(responseEntity, containsString("/cloud-infrastructure/pservers/pserver/pserver-2"));
+        verify(validationService, times(1)).validate(any());
     }
 
     @Test
@@ -559,6 +579,7 @@ public class HttpEntryTest extends AAISetup {
         assertEquals("Expected the response to be successful", 200, response.getStatus());
         assertThat("Related pserver is returned", response.getEntity().toString(),
                 containsString("\"hostname\":\"related-to-pserver\""));
+        verify(validationService, times(1)).validate(any());
 
     }
 
@@ -588,6 +609,7 @@ public class HttpEntryTest extends AAISetup {
         Response response = doRequest(traversalHttpEntry, loader, dbEngine, HttpMethod.GET, uri, requestBody);
         assertThat("Related to pserver is returned.", response.getEntity().toString(),
                 containsString("\"hostname\":\"abstract-pserver\""));
+        verify(validationService, times(1)).validate(any());
     }
 
     @Test
@@ -634,6 +656,7 @@ public class HttpEntryTest extends AAISetup {
                 relationships[0].getRelatedLink());
         assertEquals("complex.physical-location-id", relationships[0].getRelationshipData()[0].getRelationshipKey());
         assertEquals("related-to-complex", relationships[0].getRelationshipData()[0].getRelationshipValue());
+        verify(validationService, times(1)).validate(any());
     }
 
     @Test
@@ -693,6 +716,7 @@ public class HttpEntryTest extends AAISetup {
 
         JSONAssert.assertEquals(expectedResponseBody, actualResponseBody, JSONCompareMode.NON_EXTENSIBLE);
         queryParameters.remove("format");
+        verify(validationService, times(1)).validate(any());
     }
 
     @Test
@@ -810,4 +834,29 @@ public class HttpEntryTest extends AAISetup {
         int depth = traversalHttpEntry.setDepth(null, depthParam);
         assertEquals(AAIProperties.MAXIMUM_DEPTH.intValue(), depth);
     }
+
+    @Test
+    public void thatEventsAreValidated() throws AAIException, UnsupportedEncodingException {
+        String uri = "/cloud-infrastructure/pservers/pserver/theHostname";
+        traversal.addV()
+                .property("aai-node-type", "pserver")
+                .property("hostname", "theHostname")
+                .property("equip-type", "theEquipType")
+                .property(AAIProperties.AAI_URI, uri)
+                .next();
+        String requestBody = new JSONObject()
+                .put("hostname", "theHostname")
+                .put("equip-type", "theEquipType")
+                .toString();
+
+        JSONObject expectedResponseBody = new JSONObject()
+                .put("hostname", "theHostname")
+                .put("equip-type", "theEquipType");
+        Response response = doRequest(traversalHttpEntry, loader, dbEngine, HttpMethod.GET, uri, requestBody);
+        JSONObject actualResponseBody = new JSONObject(response.getEntity().toString());
+
+        JSONAssert.assertEquals(expectedResponseBody, actualResponseBody, JSONCompareMode.NON_EXTENSIBLE);
+        assertEquals("Expected the pserver to be returned", 200, response.getStatus());
+        verify(validationService, times(1)).validate(any());
+    }
 }
index aa0782b..8401681 100644 (file)
@@ -4,7 +4,7 @@
     <parent>
         <groupId>org.onap.aai.aai-common</groupId>
         <artifactId>aai-parent</artifactId>
-        <version>1.13.6-SNAPSHOT</version>
+        <version>1.14.0-SNAPSHOT</version>
         <relativePath>../aai-parent/pom.xml</relativePath>
     </parent>
     <artifactId>aai-els-onap-logging</artifactId>
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-test</artifactId>
             <scope>test</scope>
+            <exclusions>
+                               <exclusion>
+                                       <groupId>com.vaadin.external.google</groupId>
+                                       <artifactId>android-json</artifactId>
+                               </exclusion>
+                       </exclusions>
         </dependency>
         <dependency>
             <groupId>junit</groupId>
index 97548d9..bb2fe26 100644 (file)
@@ -383,8 +383,7 @@ public class ErrorLogHelper {
     }
 
     private static ErrorObject getRestErrorObject(AAIException aaiException) {
-        final int restErrorCode = Integer.parseInt(aaiException.getErrorObject().getRESTErrorCode());
-        return ErrorLogHelper.getErrorObject("AAI_" + restErrorCode);
+        return ErrorLogHelper.getErrorObject("AAI_" + aaiException.getErrorObject().getRESTErrorCode());
     }
 
     public static Fault createPolicyFault(AAIException aaiException, String text, List<String> variables) {
index ea9d44b..73890a0 100644 (file)
@@ -265,7 +265,7 @@ public class ErrorLogHelperTest {
         Info info = ErrorLogHelper.getRestApiInfoResponse(aaiExceptionsMap);
         ErrorMessage errorMessage = info.getErrorMessages().get(0);
         assertEquals("INF0001", errorMessage.getMessageId());
-        assertEquals("Internal Error (msg=%1) (ec=%2)", errorMessage.getText());
+        assertEquals("Success X-FromAppId=%1 X-TransactionId=%2 (msg=%3) (ec=%4)", errorMessage.getText());
         assertEquals("Successful health check:OK", errorMessage.getVariables().get(0));
         assertEquals("0.0.0002", errorMessage.getVariables().get(1));
     }
index 1206bdd..9395354 100644 (file)
@@ -5,7 +5,7 @@
     <parent>
         <groupId>org.onap.aai.aai-common</groupId>
         <artifactId>aai-parent</artifactId>
-        <version>1.13.6-SNAPSHOT</version>
+        <version>1.14.0-SNAPSHOT</version>
         <relativePath>../aai-parent/pom.xml</relativePath>
     </parent>
     <modelVersion>4.0.0</modelVersion>
index 25ca2a9..2fd8e88 100644 (file)
@@ -27,7 +27,7 @@ limitations under the License.
   <parent>
     <groupId>org.onap.aai.aai-common</groupId>
     <artifactId>aai-common</artifactId>
-    <version>1.13.6-SNAPSHOT</version>
+    <version>1.14.0-SNAPSHOT</version>
   </parent>
   <artifactId>aai-parent</artifactId>
   <name>aai-parent</name>
@@ -62,13 +62,13 @@ limitations under the License.
     <eelf.core.version>2.0.0-oss</eelf.core.version>
     <freemarker.version>2.3.31</freemarker.version>
     <google.guava.version>31.1-jre</google.guava.version>
-    <gremlin.version>3.2.11</gremlin.version>
+    <gremlin.version>3.3.0</gremlin.version>
+    <janusgraph.version>0.3.3</janusgraph.version>
     <groovy.version>2.5.15</groovy.version>
     <gson.version>2.9.1</gson.version>
     <hamcrest.junit.version>2.0.0.0</hamcrest.junit.version>
     <hamcrest.core.version>2.2</hamcrest.core.version>
     <jackson.bom.version>2.11.4</jackson.bom.version>
-    <janusgraph.version>0.2.3</janusgraph.version>
     <javatuples.version>1.2</javatuples.version>
     <jaxb.version>2.3.1</jaxb.version>
     <old.jaxb.version>2.3.0.1</old.jaxb.version>
index 9e8697e..72d3d30 100644 (file)
@@ -29,7 +29,7 @@
     <parent>
         <groupId>org.onap.aai.aai-common</groupId>
         <artifactId>aai-parent</artifactId>
-        <version>1.13.6-SNAPSHOT</version>
+        <version>1.14.0-SNAPSHOT</version>
         <relativePath>../aai-parent/pom.xml</relativePath>
     </parent>
     <artifactId>aai-rest</artifactId>
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-test</artifactId>
             <scope>test</scope>
+            <exclusions>
+                               <exclusion>
+                                       <groupId>com.vaadin.external.google</groupId>
+                                       <artifactId>android-json</artifactId>
+                               </exclusion>
+                       </exclusions>
         </dependency>
         <dependency>
             <groupId>org.springframework.boot</groupId>
index a0dc76c..76d4832 100644 (file)
@@ -28,7 +28,7 @@
   <parent>
     <groupId>org.onap.aai.aai-common</groupId>
     <artifactId>aai-parent</artifactId>
-    <version>1.13.6-SNAPSHOT</version>
+    <version>1.14.0-SNAPSHOT</version>
     <relativePath>../aai-parent/pom.xml</relativePath>
   </parent>
 
index dd83c92..ef9793a 100644 (file)
@@ -26,7 +26,7 @@ limitations under the License.
        <parent>
                <groupId>org.onap.aai.aai-common</groupId>
                <artifactId>aai-parent</artifactId>
-               <version>1.13.6-SNAPSHOT</version>
+               <version>1.14.0-SNAPSHOT</version>
                <relativePath>../aai-parent/pom.xml</relativePath>
        </parent>
        <artifactId>aai-schema-ingest</artifactId>
index a3d477e..cf27806 100644 (file)
@@ -140,6 +140,7 @@ public class NodeIngestor {
                 schemaPerVersion.put(version, createCombinedSchema(inputStreams, version, retrieveLocalSchema));
             }
         } catch (JAXBException | ParserConfigurationException | SAXException | IOException e) {
+            LOGGER.error("Schema ingestion failed", e);
             throw new ExceptionInInitializerError(e);
         }
     }
index 68dc4dc..cf682ed 100644 (file)
@@ -27,7 +27,7 @@
     <parent>
         <groupId>org.onap.aai.aai-common</groupId>
         <artifactId>aai-parent</artifactId>
-        <version>1.13.6-SNAPSHOT</version>
+        <version>1.14.0-SNAPSHOT</version>
         <relativePath>../aai-parent/pom.xml</relativePath>
     </parent>
     <artifactId>aai-utils</artifactId>
diff --git a/pom.xml b/pom.xml
index 1aba075..5f3b231 100644 (file)
--- a/pom.xml
+++ b/pom.xml
@@ -30,7 +30,7 @@
 
     <groupId>org.onap.aai.aai-common</groupId>
     <artifactId>aai-common</artifactId>
-    <version>1.13.6-SNAPSHOT</version>
+    <version>1.14.0-SNAPSHOT</version>
     <packaging>pom</packaging>
     <name>aai-aai-common</name>
     <description>Contains all of the common code for resources and traversal repos</description>
diff --git a/releases/1.13.6-container-release.yaml b/releases/1.13.6-container-release.yaml
new file mode 100644 (file)
index 0000000..3d30c07
--- /dev/null
@@ -0,0 +1,11 @@
+distribution_type: container
+container_release_tag: 1.13.6
+project: aai-common
+ref: 286bfbfb686254577788c8b49417e3b50ee5c858
+containers:
+    - name: aai-common-alpine
+      version: latest
+    - name: aai-common-ubuntu
+      version: latest
+    - name: aai-haproxy
+      version: latest
diff --git a/releases/1.14.0-maven-release.yaml b/releases/1.14.0-maven-release.yaml
new file mode 100644 (file)
index 0000000..d29f95a
--- /dev/null
@@ -0,0 +1,4 @@
+distribution_type: maven
+log_dir: aai-aai-common-maven-stage-master/1450/
+project: aai-common
+version: 1.14.0
index 53fad52..5dfb2dd 100644 (file)
@@ -4,8 +4,8 @@
 # because they are used in Jenkins, whose plug-in doesn't support
 
 major_version=1
-minor_version=13
-patch_version=6
+minor_version=14
+patch_version=0
 
 base_version=${major_version}.${minor_version}.${patch_version}