From c9275522f95f6cd94f28286f5e86f86dd451a19b Mon Sep 17 00:00:00 2001 From: emaclee Date: Wed, 23 Mar 2022 11:02:03 +0000 Subject: [PATCH] Add Start and Stop sessions on JAVA API Issue-ID: CPS-899 Signed-off-by: emaclee Change-Id: Idbeb922790824b1ca601d6d4798df45efa57d685 --- .../spi/impl/CpsDataPersistenceServiceImpl.java | 13 +++ .../org/onap/cps/spi/utils/SessionManager.java | 86 +++++++++++++++++ cps-ri/src/main/resources/hibernate.cfg.xml | 16 ++++ .../spi/impl/CpsDataPersistenceServiceSpec.groovy | 104 ++++++++++++--------- .../spi/utils/SessionManagerIntegrationSpec.groovy | 56 +++++++++++ cps-ri/src/test/resources/hibernate.cfg.xml | 16 ++++ .../main/java/org/onap/cps/api/CpsDataService.java | 15 +++ .../org/onap/cps/api/impl/CpsDataServiceImpl.java | 11 +++ .../onap/cps/spi/CpsDataPersistenceService.java | 13 +++ .../cps/api/impl/CpsDataServiceImplSpec.groovy | 16 ++++ 10 files changed, 302 insertions(+), 44 deletions(-) create mode 100644 cps-ri/src/main/java/org/onap/cps/spi/utils/SessionManager.java create mode 100644 cps-ri/src/main/resources/hibernate.cfg.xml create mode 100644 cps-ri/src/test/groovy/org/onap/cps/spi/utils/SessionManagerIntegrationSpec.groovy create mode 100644 cps-ri/src/test/resources/hibernate.cfg.xml diff --git a/cps-ri/src/main/java/org/onap/cps/spi/impl/CpsDataPersistenceServiceImpl.java b/cps-ri/src/main/java/org/onap/cps/spi/impl/CpsDataPersistenceServiceImpl.java index 78862d723..bb3c2d07d 100644 --- a/cps-ri/src/main/java/org/onap/cps/spi/impl/CpsDataPersistenceServiceImpl.java +++ b/cps-ri/src/main/java/org/onap/cps/spi/impl/CpsDataPersistenceServiceImpl.java @@ -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 processAncestorXpath(final List fragmentEntities, final CpsPathQuery cpsPathQuery) { final Set 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 index 000000000..eb535ecc3 --- /dev/null +++ b/cps-ri/src/main/java/org/onap/cps/spi/utils/SessionManager.java @@ -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 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 index 000000000..98e6cfc5b --- /dev/null +++ b/cps-ri/src/main/resources/hibernate.cfg.xml @@ -0,0 +1,16 @@ + + + + + + org.postgresql.Driver + jdbc:postgresql://${DB_HOST}:${DB_PORT:5432}/cpsdb + ${DB_USERNAME} + ${DB_PASSWORD} + org.hibernate.dialect.PostgreSQL82Dialect + true + update + + \ No newline at end of file diff --git a/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceServiceSpec.groovy b/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceServiceSpec.groovy index 7166008ad..c50876205 100644 --- a/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceServiceSpec.groovy +++ b/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceServiceSpec.groovy @@ -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 index 000000000..c46092f07 --- /dev/null +++ b/cps-ri/src/test/groovy/org/onap/cps/spi/utils/SessionManagerIntegrationSpec.groovy @@ -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 index 000000000..fae9275dd --- /dev/null +++ b/cps-ri/src/test/resources/hibernate.cfg.xml @@ -0,0 +1,16 @@ + + + + + + org.postgresql.Driver + ${DB_URL} + ${DB_USERNAME} + ${DB_PASSWORD} + org.hibernate.dialect.PostgreSQL82Dialect + true + none + + \ No newline at end of file diff --git a/cps-service/src/main/java/org/onap/cps/api/CpsDataService.java b/cps-service/src/main/java/org/onap/cps/api/CpsDataService.java index cdd417bd8..35caf9515 100644 --- a/cps-service/src/main/java/org/onap/cps/api/CpsDataService.java +++ b/cps-service/src/main/java/org/onap/cps/api/CpsDataService.java @@ -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); } diff --git a/cps-service/src/main/java/org/onap/cps/api/impl/CpsDataServiceImpl.java b/cps-service/src/main/java/org/onap/cps/api/impl/CpsDataServiceImpl.java index aae355d50..643614f4f 100755 --- a/cps-service/src/main/java/org/onap/cps/api/impl/CpsDataServiceImpl.java +++ b/cps-service/src/main/java/org/onap/cps/api/impl/CpsDataServiceImpl.java @@ -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) { diff --git a/cps-service/src/main/java/org/onap/cps/spi/CpsDataPersistenceService.java b/cps-service/src/main/java/org/onap/cps/spi/CpsDataPersistenceService.java index fd658861c..fdcf15bee 100644 --- a/cps-service/src/main/java/org/onap/cps/spi/CpsDataPersistenceService.java +++ b/cps-service/src/main/java/org/onap/cps/spi/CpsDataPersistenceService.java @@ -148,4 +148,17 @@ public interface CpsDataPersistenceService { Collection 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); } diff --git a/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsDataServiceImplSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsDataServiceImplSpec.groovy index 785788be9..eb06199d1 100644 --- a/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsDataServiceImplSpec.groovy +++ b/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsDataServiceImplSpec.groovy @@ -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) + } } -- 2.16.6