Merge "Add Start and Stop sessions on JAVA API"
authorToine Siebelink <toine.siebelink@est.tech>
Fri, 25 Mar 2022 18:29:31 +0000 (18:29 +0000)
committerGerrit Code Review <gerrit@onap.org>
Fri, 25 Mar 2022 18:29:31 +0000 (18:29 +0000)
cps-ri/src/main/java/org/onap/cps/spi/impl/CpsDataPersistenceServiceImpl.java
cps-ri/src/main/java/org/onap/cps/spi/utils/SessionManager.java [new file with mode: 0644]
cps-ri/src/main/resources/hibernate.cfg.xml [new file with mode: 0644]
cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceServiceSpec.groovy
cps-ri/src/test/groovy/org/onap/cps/spi/utils/SessionManagerIntegrationSpec.groovy [new file with mode: 0644]
cps-ri/src/test/resources/hibernate.cfg.xml [new file with mode: 0644]
cps-service/src/main/java/org/onap/cps/api/CpsDataService.java
cps-service/src/main/java/org/onap/cps/api/impl/CpsDataServiceImpl.java
cps-service/src/main/java/org/onap/cps/spi/CpsDataPersistenceService.java
cps-service/src/test/groovy/org/onap/cps/api/impl/CpsDataServiceImplSpec.groovy

index 78862d7..bb3c2d0 100644 (file)
@@ -56,6 +56,7 @@ import org.onap.cps.spi.model.DataNodeBuilder;
 import org.onap.cps.spi.repository.AnchorRepository;
 import org.onap.cps.spi.repository.DataspaceRepository;
 import org.onap.cps.spi.repository.FragmentRepository;
+import org.onap.cps.spi.utils.SessionManager;
 import org.onap.cps.utils.JsonObjectMapper;
 import org.springframework.dao.DataIntegrityViolationException;
 import org.springframework.stereotype.Service;
@@ -73,6 +74,8 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService
 
     private final JsonObjectMapper jsonObjectMapper;
 
+    private final SessionManager sessionManager;
+
     private static final String REG_EX_FOR_OPTIONAL_LIST_INDEX = "(\\[@[\\s\\S]+?]){0,1})";
     private static final Pattern REG_EX_PATTERN_FOR_LIST_ELEMENT_KEY_PREDICATE =
             Pattern.compile("\\[(\\@([^\\/]{0,9999}))\\]$");
@@ -199,6 +202,16 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService
             .collect(Collectors.toUnmodifiableList());
     }
 
+    @Override
+    public String startSession() {
+        return sessionManager.startSession();
+    }
+
+    @Override
+    public void closeSession(final String sessionId) {
+        sessionManager.closeSession(sessionId);
+    }
+
     private static Set<String> processAncestorXpath(final List<FragmentEntity> fragmentEntities,
         final CpsPathQuery cpsPathQuery) {
         final Set<String> ancestorXpath = new HashSet<>();
diff --git a/cps-ri/src/main/java/org/onap/cps/spi/utils/SessionManager.java b/cps-ri/src/main/java/org/onap/cps/spi/utils/SessionManager.java
new file mode 100644 (file)
index 0000000..eb535ec
--- /dev/null
@@ -0,0 +1,86 @@
+/*
+ *  ============LICENSE_START=======================================================
+ *  Copyright (C) 2021-2022 Nordix Foundation
+ *  ================================================================================
+ *  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.
+ *
+ *  SPDX-License-Identifier: Apache-2.0
+ *  ============LICENSE_END=========================================================
+ */
+
+package org.onap.cps.spi.utils;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+import org.hibernate.HibernateException;
+import org.hibernate.Session;
+import org.hibernate.SessionException;
+import org.hibernate.SessionFactory;
+import org.hibernate.cfg.Configuration;
+import org.onap.cps.spi.entities.AnchorEntity;
+import org.onap.cps.spi.entities.DataspaceEntity;
+import org.onap.cps.spi.entities.SchemaSetEntity;
+import org.onap.cps.spi.entities.YangResourceEntity;
+import org.springframework.stereotype.Component;
+
+@Component
+public class SessionManager {
+
+    private static SessionFactory sessionFactory;
+    private static Map<String, Session> sessionMap = new HashMap<>();
+
+    private synchronized void buildSessionFactory() {
+        if (sessionFactory == null) {
+            sessionFactory = new Configuration().configure("hibernate.cfg.xml")
+                    .addAnnotatedClass(AnchorEntity.class)
+                    .addAnnotatedClass(DataspaceEntity.class)
+                    .addAnnotatedClass(SchemaSetEntity.class)
+                    .addAnnotatedClass(YangResourceEntity.class)
+                    .buildSessionFactory();
+        }
+    }
+
+    /**
+     * Starts a session which allows use of locks and batch interaction with the persistence service.
+     *
+     * @return Session ID string
+     */
+    public String startSession() {
+        buildSessionFactory();
+        final Session session = sessionFactory.openSession();
+        final String sessionId = UUID.randomUUID().toString();
+        sessionMap.put(sessionId, session);
+        session.beginTransaction();
+        return sessionId;
+    }
+
+    /**
+     * Close session.
+     *
+     * @param sessionId session ID
+     */
+    public void closeSession(final String sessionId) {
+        try {
+            final Session currentSession = sessionMap.get(sessionId);
+            currentSession.getTransaction().commit();
+            currentSession.close();
+        } catch (final NullPointerException e) {
+            throw new SessionException(String.format("Session with session ID %s does not exist", sessionId));
+        } catch (final HibernateException e) {
+            throw new SessionException(String.format("Unable to close session with session ID %s", sessionId));
+        }
+        sessionMap.remove(sessionId);
+    }
+
+}
\ No newline at end of file
diff --git a/cps-ri/src/main/resources/hibernate.cfg.xml b/cps-ri/src/main/resources/hibernate.cfg.xml
new file mode 100644 (file)
index 0000000..98e6cfc
--- /dev/null
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE hibernate-configuration PUBLIC
+        "-//Hibernate/Hibernate Configuration DTD 3.0//EN"
+        "http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd">
+
+<hibernate-configuration>
+    <session-factory>
+        <property name="hibernate.connection.driver_class">org.postgresql.Driver</property>
+        <property name="hibernate.connection.url">jdbc:postgresql://${DB_HOST}:${DB_PORT:5432}/cpsdb</property>
+        <property name="hibernate.connection.username">${DB_USERNAME}</property>
+        <property name="hibernate.connection.password">${DB_PASSWORD}</property>
+        <property name="hibernate.dialect">org.hibernate.dialect.PostgreSQL82Dialect</property>
+        <property name="show_sql">true</property>
+        <property name="hibernate.hbm2ddl.auto">update</property>
+    </session-factory>
+</hibernate-configuration>
\ No newline at end of file
index 7166008..c508762 100644 (file)
@@ -28,19 +28,20 @@ import org.onap.cps.spi.model.DataNodeBuilder
 import org.onap.cps.spi.repository.AnchorRepository
 import org.onap.cps.spi.repository.DataspaceRepository
 import org.onap.cps.spi.repository.FragmentRepository
+import org.onap.cps.spi.utils.SessionManager
 import org.onap.cps.utils.JsonObjectMapper
 import spock.lang.Specification
 
-
 class CpsDataPersistenceServiceSpec extends Specification {
 
     def mockDataspaceRepository = Mock(DataspaceRepository)
     def mockAnchorRepository = Mock(AnchorRepository)
     def mockFragmentRepository = Mock(FragmentRepository)
     def jsonObjectMapper = new JsonObjectMapper(new ObjectMapper())
+    def mockSessionManager = Mock(SessionManager)
 
     def objectUnderTest = new CpsDataPersistenceServiceImpl(
-            mockDataspaceRepository, mockAnchorRepository, mockFragmentRepository, jsonObjectMapper)
+            mockDataspaceRepository, mockAnchorRepository, mockFragmentRepository, jsonObjectMapper,mockSessionManager)
 
     def 'Handling of StaleStateException (caused by concurrent updates) during data node tree update.'() {
 
@@ -49,67 +50,82 @@ class CpsDataPersistenceServiceSpec extends Specification {
         def myAnchorName = 'my-anchor'
 
         given: 'data node object'
-            def submittedDataNode = new DataNodeBuilder()
-                    .withXpath(parentXpath)
-                    .withLeaves(['leaf-name': 'leaf-value'])
-                    .build()
+        def submittedDataNode = new DataNodeBuilder()
+                .withXpath(parentXpath)
+                .withLeaves(['leaf-name': 'leaf-value'])
+                .build()
         and: 'fragment to be updated'
-            mockFragmentRepository.getByDataspaceAndAnchorAndXpath(_, _, _) >> {
-                def fragmentEntity = new FragmentEntity()
-                fragmentEntity.setXpath(parentXpath)
-                fragmentEntity.setChildFragments(Collections.emptySet())
-                return fragmentEntity
-            }
+        mockFragmentRepository.getByDataspaceAndAnchorAndXpath(_, _, _) >> {
+            def fragmentEntity = new FragmentEntity()
+            fragmentEntity.setXpath(parentXpath)
+            fragmentEntity.setChildFragments(Collections.emptySet())
+            return fragmentEntity
+        }
         and: 'data node is concurrently updated by another transaction'
-            mockFragmentRepository.save(_) >> { throw new StaleStateException("concurrent updates") }
+        mockFragmentRepository.save(_) >> { throw new StaleStateException("concurrent updates") }
 
         when: 'attempt to update data node'
-            objectUnderTest.replaceDataNodeTree(myDataspaceName, myAnchorName, submittedDataNode)
+        objectUnderTest.replaceDataNodeTree(myDataspaceName, myAnchorName, submittedDataNode)
 
         then: 'concurrency exception is thrown'
-            def concurrencyException = thrown(ConcurrencyException)
-            assert concurrencyException.getDetails().contains(myDataspaceName)
-            assert concurrencyException.getDetails().contains(myAnchorName)
-            assert concurrencyException.getDetails().contains(parentXpath)
+        def concurrencyException = thrown(ConcurrencyException)
+        assert concurrencyException.getDetails().contains(myDataspaceName)
+        assert concurrencyException.getDetails().contains(myAnchorName)
+        assert concurrencyException.getDetails().contains(parentXpath)
     }
 
     def 'Retrieving a data node with a property JSON value of #scenario'() {
         given: 'a fragment with a property JSON value of #scenario'
-            mockFragmentRepository.getByDataspaceAndAnchorAndXpath(_, _, _) >> {
-                new FragmentEntity(childFragments: Collections.emptySet(),
-                        attributes: "{\"some attribute\": ${dataString}}")
-            }
+        mockFragmentRepository.getByDataspaceAndAnchorAndXpath(_, _, _) >> {
+            new FragmentEntity(childFragments: Collections.emptySet(),
+                    attributes: "{\"some attribute\": ${dataString}}")
+        }
         when: 'getting the data node represented by this fragment'
-            def dataNode = objectUnderTest.getDataNode('my-dataspace', 'my-anchor',
-                    'parent-01', FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS)
+        def dataNode = objectUnderTest.getDataNode('my-dataspace', 'my-anchor',
+                'parent-01', FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS)
         then: 'the leaf is of the correct value and data type'
-            def attributeValue = dataNode.leaves.get('some attribute')
-            assert attributeValue == expectedValue
-            assert attributeValue.class == expectedDataClass
+        def attributeValue = dataNode.leaves.get('some attribute')
+        assert attributeValue == expectedValue
+        assert attributeValue.class == expectedDataClass
         where: 'the following Data Type is passed'
-            scenario                              | dataString            || expectedValue     | expectedDataClass
-            'just numbers'                        | '15174'               || 15174             | Integer
-            'number with dot'                     | '15174.32'            || 15174.32          | Double
-            'number with 0 value after dot'       | '15174.0'             || 15174.0           | Double
-            'number with 0 value before dot'      | '0.32'                || 0.32              | Double
-            'number higher than max int'          | '2147483648'          || 2147483648        | Long
-            'just text'                           | '"Test"'              || 'Test'            | String
-            'number with exponent'                | '1.2345e5'            || 1.2345e5          | Double
-            'number higher than max int with dot' | '123456789101112.0'   || 123456789101112.0 | Double
-            'text and numbers'                    | '"String = \'1234\'"' || "String = '1234'" | String
-            'number as String'                    | '"12345"'             || '12345'           | String
+        scenario                              | dataString            || expectedValue     | expectedDataClass
+        'just numbers'                        | '15174'               || 15174             | Integer
+        'number with dot'                     | '15174.32'            || 15174.32          | Double
+        'number with 0 value after dot'       | '15174.0'             || 15174.0           | Double
+        'number with 0 value before dot'      | '0.32'                || 0.32              | Double
+        'number higher than max int'          | '2147483648'          || 2147483648        | Long
+        'just text'                           | '"Test"'              || 'Test'            | String
+        'number with exponent'                | '1.2345e5'            || 1.2345e5          | Double
+        'number higher than max int with dot' | '123456789101112.0'   || 123456789101112.0 | Double
+        'text and numbers'                    | '"String = \'1234\'"' || "String = '1234'" | String
+        'number as String'                    | '"12345"'             || '12345'           | String
     }
 
     def 'Retrieving a data node with invalid JSON'() {
         given: 'a fragment with invalid JSON'
-            mockFragmentRepository.getByDataspaceAndAnchorAndXpath(_, _, _) >> {
-                new FragmentEntity(childFragments: Collections.emptySet(), attributes: '{invalid json')
-            }
+        mockFragmentRepository.getByDataspaceAndAnchorAndXpath(_, _, _) >> {
+            new FragmentEntity(childFragments: Collections.emptySet(), attributes: '{invalid json')
+        }
         when: 'getting the data node represented by this fragment'
-            def dataNode = objectUnderTest.getDataNode('my-dataspace', 'my-anchor',
+        def dataNode = objectUnderTest.getDataNode('my-dataspace', 'my-anchor',
                 'parent-01', FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS)
         then: 'a data validation exception is thrown'
-            thrown(DataValidationException)
+        thrown(DataValidationException)
     }
 
-}
+    def 'start session'() {
+        when: 'start session'
+            objectUnderTest.startSession()
+        then: 'the session manager method to start session is invoked'
+            1 * mockSessionManager.startSession()
+    }
+
+    def 'close session'() {
+        given: 'session ID'
+            def someSessionId = 'someSessionId'
+        when: 'close session method is called with session ID as parameter'
+            objectUnderTest.closeSession(someSessionId)
+        then: 'the session manager method to close session is invoked with parameter'
+            1 * mockSessionManager.closeSession(someSessionId)
+    }
+}
\ No newline at end of file
diff --git a/cps-ri/src/test/groovy/org/onap/cps/spi/utils/SessionManagerIntegrationSpec.groovy b/cps-ri/src/test/groovy/org/onap/cps/spi/utils/SessionManagerIntegrationSpec.groovy
new file mode 100644 (file)
index 0000000..c46092f
--- /dev/null
@@ -0,0 +1,56 @@
+/*
+ *  ============LICENSE_START=======================================================
+ *  Copyright (C) 2021-2022 Nordix Foundation
+ *  ================================================================================
+ *  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.
+ *
+ *  SPDX-License-Identifier: Apache-2.0
+ *  ============LICENSE_END=========================================================
+ */
+
+package org.onap.cps.spi.utils
+
+import org.hibernate.SessionException
+import org.onap.cps.spi.impl.CpsPersistenceSpecBase
+
+class SessionManagerIntegrationSpec extends CpsPersistenceSpecBase{
+
+    def objectUnderTest = new SessionManager();
+
+    def 'start session'() {
+        when: 'start session'
+            def result = objectUnderTest.startSession()
+        then: 'session ID is returned'
+            assert result instanceof String
+            objectUnderTest.closeSession(result)
+    }
+
+    def 'close session'(){
+        given: 'session Id from calling the start session method'
+            def sessionId = objectUnderTest.startSession()
+        when: 'close session method is called'
+            objectUnderTest.closeSession(sessionId)
+        then: 'no exception is thrown'
+            noExceptionThrown()
+    }
+
+    def 'close session that does not exist' (){
+        given: 'session Id that does not exist'
+            def unknownSessionId = 'unknown session id'
+        when: 'close session method is called'
+            objectUnderTest.closeSession(unknownSessionId)
+        then: 'a session exception is thrown'
+            def thrown = thrown(SessionException)
+            assert thrown.message.contains(unknownSessionId)
+    }
+}
diff --git a/cps-ri/src/test/resources/hibernate.cfg.xml b/cps-ri/src/test/resources/hibernate.cfg.xml
new file mode 100644 (file)
index 0000000..fae9275
--- /dev/null
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8"?>\r
+<!DOCTYPE hibernate-configuration PUBLIC\r
+        "-//Hibernate/Hibernate Configuration DTD 3.0//EN"\r
+        "http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd">\r
+\r
+<hibernate-configuration>\r
+    <session-factory>\r
+        <property name="hibernate.connection.driver_class">org.postgresql.Driver</property>\r
+        <property name="hibernate.connection.url">${DB_URL}</property>\r
+        <property name="hibernate.connection.username">${DB_USERNAME}</property>\r
+        <property name="hibernate.connection.password">${DB_PASSWORD}</property>\r
+        <property name="hibernate.dialect">org.hibernate.dialect.PostgreSQL82Dialect</property>\r
+        <property name="show_sql">true</property>\r
+        <property name="hibernate.hbm2ddl.auto">none</property>\r
+    </session-factory>\r
+</hibernate-configuration>
\ No newline at end of file
index cdd417b..35caf95 100644 (file)
@@ -174,4 +174,19 @@ public interface CpsDataService {
      */
     void updateNodeLeavesAndExistingDescendantLeaves(String dataspaceName, String anchorName, String parentNodeXpath,
         String dataNodeUpdatesAsJson, OffsetDateTime observedTimestamp);
+
+    /**
+     * Starts a session which allows use of locks and batch interaction with the persistence service.
+     *
+     * @return Session ID string
+     */
+    String startSession();
+
+    /**
+     * Close session.
+     *
+     * @param sessionId session ID
+     *
+     */
+    void closeSession(String sessionId);
 }
index aae355d..643614f 100755 (executable)
@@ -108,6 +108,17 @@ public class CpsDataServiceImpl implements CpsDataService {
         processDataUpdatedEventAsync(dataspaceName, anchorName, observedTimestamp, parentNodeXpath, Operation.UPDATE);
     }
 
+    @Override
+    public String startSession() {
+        final String sessionId = cpsDataPersistenceService.startSession();
+        return sessionId;
+    }
+
+    @Override
+    public void closeSession(final String sessionId) {
+        cpsDataPersistenceService.closeSession(sessionId);
+    }
+
     @Override
     public void replaceNodeTree(final String dataspaceName, final String anchorName, final String parentNodeXpath,
         final String jsonData, final OffsetDateTime observedTimestamp) {
index fd65886..fdcf15b 100644 (file)
@@ -148,4 +148,17 @@ public interface CpsDataPersistenceService {
     Collection<DataNode> queryDataNodes(String dataspaceName, String anchorName,
         String cpsPath, FetchDescendantsOption fetchDescendantsOption);
 
+    /**
+     * Starts a session which allows use of locks and batch interaction with the persistence service.
+     *
+     * @return Session ID string
+     */
+    String startSession();
+
+    /**
+     * Close session.
+     *
+     * @param sessionId session ID
+     */
+    void closeSession(String sessionId);
 }
index 785788b..eb06199 100644 (file)
@@ -254,4 +254,20 @@ class CpsDataServiceImplSpec extends Specification {
         def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourceNameToContent).getSchemaContext()
         mockYangTextSchemaSourceSet.getSchemaContext() >> schemaContext
     }
+
+    def 'start session'() {
+        when: 'start session method is called'
+            objectUnderTest.startSession()
+        then: 'the persistence service method to start session is invoked'
+            1 * mockCpsDataPersistenceService.startSession()
+    }
+
+    def 'close session'(){
+        given: 'session Id from calling the start session method'
+            def sessionId = objectUnderTest.startSession()
+        when: 'close session method is called'
+            objectUnderTest.closeSession(sessionId)
+        then: 'the persistence service method to close session is invoked'
+            1 * mockCpsDataPersistenceService.closeSession(sessionId)
+    }
 }