summaryrefslogtreecommitdiffstats
path: root/common-be/src/test/java
diff options
context:
space:
mode:
Diffstat (limited to 'common-be/src/test/java')
-rw-r--r--common-be/src/test/java/org/openecomp/sdc/be/csar/security/CertificateManagerImplTest.java141
-rw-r--r--common-be/src/test/java/org/openecomp/sdc/be/csar/security/PrivateKeyReaderImplTest.java95
-rw-r--r--common-be/src/test/java/org/openecomp/sdc/be/csar/security/Sha256WithRsaCmsContentSignerTest.java119
-rw-r--r--common-be/src/test/java/org/openecomp/sdc/be/csar/security/X509CertificateReaderTest.java81
-rw-r--r--common-be/src/test/java/org/openecomp/sdc/be/csar/security/model/CertificateInfoImplTest.java69
5 files changed, 505 insertions, 0 deletions
diff --git a/common-be/src/test/java/org/openecomp/sdc/be/csar/security/CertificateManagerImplTest.java b/common-be/src/test/java/org/openecomp/sdc/be/csar/security/CertificateManagerImplTest.java
new file mode 100644
index 0000000000..6287c0e85b
--- /dev/null
+++ b/common-be/src/test/java/org/openecomp/sdc/be/csar/security/CertificateManagerImplTest.java
@@ -0,0 +1,141 @@
+/*
+ * ============LICENSE_START=======================================================
+ * Copyright (C) 2021 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.openecomp.sdc.be.csar.security;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.notNullValue;
+import static org.junit.jupiter.api.Assertions.fail;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.when;
+import static org.openecomp.sdc.be.csar.security.CertificateManagerImpl.CERT_DIR_ENV_VARIABLE;
+
+import java.net.URL;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.security.cert.CertificateExpiredException;
+import java.security.cert.CertificateNotYetValidException;
+import java.security.cert.X509Certificate;
+import java.util.Optional;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentMatchers;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.openecomp.sdc.be.csar.security.api.CertificateReader;
+import org.openecomp.sdc.be.csar.security.api.PrivateKeyReader;
+import org.openecomp.sdc.be.csar.security.api.model.CertificateInfo;
+import org.springframework.core.env.Environment;
+
+class CertificateManagerImplTest {
+
+ @Mock
+ private Environment environment;
+ @Mock
+ private PrivateKeyReader privateKeyReader;
+ @Mock
+ private CertificateReader certificateReader;
+ @Mock
+ private X509Certificate certificateMock;
+ private CertificateManagerImpl certificateManager;
+
+ static Path certificateFolderPath;
+
+ @BeforeAll
+ static void beforeAll() {
+ final String resourceFolder = "certificateManager";
+ final URL certificateManager = CertificateManagerImplTest.class.getClassLoader().getResource(resourceFolder);
+ if (certificateManager == null) {
+ fail("Could not find resource folder " + resourceFolder);
+ }
+ certificateFolderPath = Paths.get(certificateManager.getPath());
+ }
+
+ @BeforeEach
+ void setUp() throws CertificateNotYetValidException, CertificateExpiredException {
+ MockitoAnnotations.initMocks(this);
+ when(environment.getProperty(CERT_DIR_ENV_VARIABLE)).thenReturn(certificateFolderPath.toString());
+ when(certificateMock.getType()).thenReturn("X.509");
+ doNothing().when(certificateMock).checkValidity();
+ when(certificateReader.loadCertificate(ArgumentMatchers.any())).thenReturn(certificateMock);
+ certificateManager = new CertificateManagerImpl(privateKeyReader, certificateReader, environment);
+ }
+
+ @Test
+ void getCertificateSuccessTest() {
+ final String certificateName = "fakeCert1";
+ final Optional<CertificateInfo> certificateOpt = certificateManager.getCertificate(certificateName);
+ assertThat(certificateOpt.isPresent(), is(true));
+ final CertificateInfo certificateInfo = certificateOpt.get();
+ assertThat(certificateInfo.getName(), is(certificateName));
+ assertThat(certificateInfo.getPrivateKeyFile(), is(notNullValue()));
+ assertThat(certificateInfo.getPrivateKeyFile().getAbsolutePath(),
+ is(certificateFolderPath.resolve(certificateName + ".key").toString()));
+ assertThat(certificateInfo.getCertificateFile(), is(notNullValue()));
+ assertThat(certificateInfo.getCertificateFile().getAbsolutePath(),
+ is(certificateFolderPath.resolve(certificateName + ".cert").toString()));
+ }
+
+ @Test
+ void initCertificateSuccessTest() {
+ final String certificateName1 = "fakeCert1";
+ final String certificateName2 = "fakeCert2";
+ final String certificateName3 = "fakeCert3";
+ assertThat("Certificate " + certificateName1 + " should be present",
+ certificateManager.getCertificate(certificateName1).isPresent(), is(true));
+ assertThat("Certificate " + certificateName2 + " should be present",
+ certificateManager.getCertificate(certificateName2).isPresent(), is(true));
+ assertThat("Certificate " + certificateName3 + " should not be present",
+ certificateManager.getCertificate(certificateName3).isEmpty(), is(true));
+ }
+
+ @Test
+ void invalidCertificateFolderTest() {
+ final String certificateName1 = "fakeCert1";
+ when(environment.getProperty(CERT_DIR_ENV_VARIABLE)).thenReturn("/an/invalid/folder");
+ final CertificateManagerImpl certificateManager =
+ new CertificateManagerImpl(privateKeyReader, certificateReader, environment);
+ assertThat("Certificate " + certificateName1 + " should be present",
+ certificateManager.getCertificate(certificateName1).isPresent(), is(false));
+ }
+
+ @Test
+ void noEnvironmentVariableConfiguredTest() {
+ final String certificateName1 = "fakeCert1";
+ when(environment.getProperty(CERT_DIR_ENV_VARIABLE)).thenReturn(null);
+ final CertificateManagerImpl certificateManager =
+ new CertificateManagerImpl(privateKeyReader, certificateReader, environment);
+ assertThat("Certificate " + certificateName1 + " should be present",
+ certificateManager.getCertificate(certificateName1).isPresent(), is(false));
+ }
+
+ @Test
+ void loadCertificateExceptionTest() {
+ final String certificateName1 = "fakeCert1";
+ when(certificateReader.loadCertificate(any())).thenThrow(new RuntimeException());
+ final CertificateManagerImpl certificateManager =
+ new CertificateManagerImpl(privateKeyReader, certificateReader, environment);
+ assertThat("Certificate " + certificateName1 + " should be present",
+ certificateManager.getCertificate(certificateName1).isPresent(), is(false));
+ }
+
+} \ No newline at end of file
diff --git a/common-be/src/test/java/org/openecomp/sdc/be/csar/security/PrivateKeyReaderImplTest.java b/common-be/src/test/java/org/openecomp/sdc/be/csar/security/PrivateKeyReaderImplTest.java
new file mode 100644
index 0000000000..7bd44cf9c1
--- /dev/null
+++ b/common-be/src/test/java/org/openecomp/sdc/be/csar/security/PrivateKeyReaderImplTest.java
@@ -0,0 +1,95 @@
+/*
+ * ============LICENSE_START=======================================================
+ * Copyright (C) 2021 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.openecomp.sdc.be.csar.security;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.io.File;
+import java.net.URL;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.security.Key;
+import java.security.Security;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.openecomp.sdc.be.csar.security.exception.LoadPrivateKeyException;
+import org.openecomp.sdc.be.csar.security.exception.UnsupportedKeyFormatException;
+
+class PrivateKeyReaderImplTest {
+
+ private PrivateKeyReaderImpl privateKeyReader;
+
+ @BeforeEach
+ void setUp() {
+ privateKeyReader = new PrivateKeyReaderImpl();
+ if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
+ Security.addProvider(new BouncyCastleProvider());
+ }
+ }
+
+ @AfterEach
+ void tearDown() {
+ if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) != null) {
+ Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME);
+ }
+ }
+
+ @Test
+ void loadPrivateKeySuccessTest() {
+ final Path certPath = Paths.get("certificateManager", "realCert", "realCert1.key");
+ final URL resource = getClass().getClassLoader().getResource(certPath.toString());
+ if (resource == null) {
+ fail("Could not find resource " + certPath.toString());
+ }
+ final Key privateKey = privateKeyReader.loadPrivateKey(new File(resource.getPath()));
+ assertNotNull(privateKey);
+ }
+
+ @Test
+ void loadInvalidKeyFilePathTest() {
+ final String invalidFilePath = "aaaa";
+ final File keyFile = new File(invalidFilePath);
+ final LoadPrivateKeyException actualException = assertThrows(LoadPrivateKeyException.class,
+ () -> privateKeyReader.loadPrivateKey(keyFile));
+ assertThat(actualException.getMessage(),
+ is(String.format("Could not load the private key from given file '%s'", invalidFilePath)));
+ }
+
+ @Test
+ void loadInvalidKeyFileTest() {
+ final Path certPath = Paths.get("certificateManager", "fakeCert1.key");
+ final URL resource = getClass().getClassLoader().getResource(certPath.toString());
+ if (resource == null) {
+ fail("Could not find resource " + certPath.toString());
+ }
+ final File keyFile = new File(resource.getPath());
+ final UnsupportedKeyFormatException actualException = assertThrows(UnsupportedKeyFormatException.class,
+ () -> privateKeyReader.loadPrivateKey(keyFile));
+ assertThat(actualException.getMessage(),
+ is(String.format("Could not load the private key from given file '%s'. Unsupported format.",
+ resource.getPath())));
+ }
+} \ No newline at end of file
diff --git a/common-be/src/test/java/org/openecomp/sdc/be/csar/security/Sha256WithRsaCmsContentSignerTest.java b/common-be/src/test/java/org/openecomp/sdc/be/csar/security/Sha256WithRsaCmsContentSignerTest.java
new file mode 100644
index 0000000000..2f0031d6e1
--- /dev/null
+++ b/common-be/src/test/java/org/openecomp/sdc/be/csar/security/Sha256WithRsaCmsContentSignerTest.java
@@ -0,0 +1,119 @@
+/*
+ * ============LICENSE_START=======================================================
+ * Copyright (C) 2021 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.openecomp.sdc.be.csar.security;
+
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.URL;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.security.Key;
+import java.security.Security;
+import java.security.cert.Certificate;
+import java.security.cert.X509Certificate;
+import org.bouncycastle.cms.CMSException;
+import org.bouncycastle.cms.CMSProcessableByteArray;
+import org.bouncycastle.cms.CMSSignedData;
+import org.bouncycastle.cms.SignerInformation;
+import org.bouncycastle.cms.SignerInformationStore;
+import org.bouncycastle.cms.SignerInformationVerifier;
+import org.bouncycastle.cms.jcajce.JcaSimpleSignerInfoVerifierBuilder;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.bouncycastle.operator.OperatorCreationException;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.openecomp.sdc.be.csar.security.api.CertificateReader;
+import org.openecomp.sdc.be.csar.security.api.PrivateKeyReader;
+import org.openecomp.sdc.be.csar.security.exception.CmsSignatureException;
+
+class Sha256WithRsaCmsContentSignerTest {
+
+ private Sha256WithRsaCmsContentSigner cmsContentSigner;
+ private PrivateKeyReader privateKeyReader;
+ private CertificateReader certificateReader;
+
+ private static final Path testFilesPath = Path.of("certificateManager", "signerTest");
+ private static final Path certFilesPath = Path.of("certificateManager", "realCert");
+
+ @BeforeEach
+ void setUp() {
+ Security.addProvider(new BouncyCastleProvider());
+ cmsContentSigner = new Sha256WithRsaCmsContentSigner();
+ privateKeyReader = new PrivateKeyReaderImpl();
+ certificateReader = new X509CertificateReader();
+ }
+
+ @AfterEach
+ void tearDown() {
+ Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME);
+ }
+
+ @Test
+ void signDataSuccessTest() throws OperatorCreationException, CMSException, IOException, CmsSignatureException {
+ final File certFile = getResourceFile(certFilesPath.resolve("realCert1.cert"));
+ final File keyFile = getResourceFile(certFilesPath.resolve("realCert1.key"));
+ final File fileToSign = getResourceFile(testFilesPath.resolve("fileToSign.txt"));
+ final Key privateKey = privateKeyReader.loadPrivateKey(keyFile);
+ final Certificate certificate = certificateReader.loadCertificate(certFile);
+ final byte[] actualSignatureBytes = cmsContentSigner
+ .signData(Files.readAllBytes(fileToSign.toPath()), certificate, privateKey);
+
+ assertTrue(verifySignature(Files.readAllBytes(fileToSign.toPath()), actualSignatureBytes,
+ (X509Certificate) certificate));
+ }
+
+ @Test
+ void signDataInvalidCertAndKeyTest() {
+ assertThrows(CmsSignatureException.class,
+ () -> cmsContentSigner.signData(null, null, null));
+ }
+
+ private boolean verifySignature(byte[] contentBytes, byte[] signatureBytes, X509Certificate certificate)
+ throws CMSException, OperatorCreationException {
+
+ final CMSSignedData cms = new CMSSignedData(new CMSProcessableByteArray(contentBytes), signatureBytes);
+ final SignerInformationStore signers = cms.getSignerInfos();
+ final SignerInformationVerifier signerInformationVerifier =
+ new JcaSimpleSignerInfoVerifierBuilder()
+ .setProvider(BouncyCastleProvider.PROVIDER_NAME).build(certificate);
+ for (final SignerInformation signer : signers.getSigners()) {
+ if (!signer.verify(signerInformationVerifier)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ private File getResourceFile(final Path testResourcePath) {
+ final URL resource = getClass().getClassLoader().getResource(testResourcePath.toString());
+ if (resource == null) {
+ fail("Could not load the file " + testResourcePath.toString());
+ }
+
+ return new File(resource.getPath());
+ }
+
+} \ No newline at end of file
diff --git a/common-be/src/test/java/org/openecomp/sdc/be/csar/security/X509CertificateReaderTest.java b/common-be/src/test/java/org/openecomp/sdc/be/csar/security/X509CertificateReaderTest.java
new file mode 100644
index 0000000000..3235739780
--- /dev/null
+++ b/common-be/src/test/java/org/openecomp/sdc/be/csar/security/X509CertificateReaderTest.java
@@ -0,0 +1,81 @@
+/*
+ * ============LICENSE_START=======================================================
+ * Copyright (C) 2021 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.openecomp.sdc.be.csar.security;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.io.File;
+import java.net.URL;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.security.cert.Certificate;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.openecomp.sdc.be.csar.security.exception.LoadCertificateException;
+
+class X509CertificateReaderTest {
+
+ private X509CertificateReader certificateReader;
+
+ @BeforeEach
+ void setUp() {
+ certificateReader = new X509CertificateReader();
+ }
+
+ @Test
+ void loadCertificateSuccessTest() {
+ final Path certPath = Paths.get("certificateManager", "realCert", "realCert1.cert");
+ final URL resource = getClass().getClassLoader().getResource(certPath.toString());
+ if (resource == null) {
+ fail("Could not find resource " + certPath.toString());
+ }
+ final Certificate certificate = certificateReader.loadCertificate(new File(resource.getPath()));
+ assertNotNull(certificate);
+ }
+
+ @Test
+ void loadInvalidCertificateFilePathTest() {
+ final String invalidFilePath = "aaaa";
+ final File certFile = new File(invalidFilePath);
+ final LoadCertificateException actualException = assertThrows(LoadCertificateException.class,
+ () -> certificateReader.loadCertificate(certFile));
+ assertThat(actualException.getMessage(),
+ is(String.format("Could not load the certificate from given file '%s'", invalidFilePath)));
+ }
+
+ @Test
+ void loadInvalidCertificateFileTest() {
+ final Path certPath = Paths.get("certificateManager", "fakeCert1.cert");
+ System.out.println(certPath.toString());
+ final URL resource = getClass().getClassLoader().getResource(certPath.toString());
+ if (resource == null) {
+ fail("Could not find resource " + certPath.toString());
+ }
+ final File certFile = new File(resource.getPath());
+ final LoadCertificateException actualException = assertThrows(LoadCertificateException.class,
+ () -> certificateReader.loadCertificate(certFile));
+ assertThat(actualException.getMessage(),
+ is(String.format("Could not load the certificate from given file '%s'", resource.getPath())));
+ }
+} \ No newline at end of file
diff --git a/common-be/src/test/java/org/openecomp/sdc/be/csar/security/model/CertificateInfoImplTest.java b/common-be/src/test/java/org/openecomp/sdc/be/csar/security/model/CertificateInfoImplTest.java
new file mode 100644
index 0000000000..6b094130b0
--- /dev/null
+++ b/common-be/src/test/java/org/openecomp/sdc/be/csar/security/model/CertificateInfoImplTest.java
@@ -0,0 +1,69 @@
+/*
+ * ============LICENSE_START=======================================================
+ * Copyright (C) 2021 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.openecomp.sdc.be.csar.security.model;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.when;
+
+import java.io.File;
+import java.security.cert.CertificateExpiredException;
+import java.security.cert.CertificateNotYetValidException;
+import java.security.cert.X509Certificate;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+class CertificateInfoImplTest {
+
+ @Mock
+ private X509Certificate certificate;
+
+ @BeforeEach
+ void setUp() {
+ MockitoAnnotations.initMocks(this);
+ }
+
+ @Test
+ void isValidTest() throws CertificateNotYetValidException, CertificateExpiredException {
+ when(certificate.getType()).thenReturn("X.509");
+ doNothing().when(certificate).checkValidity();
+ final CertificateInfoImpl certificateInfo = new CertificateInfoImpl(new File(""), certificate);
+ assertTrue(certificateInfo.isValid());
+ doThrow(CertificateExpiredException.class).when(certificate).checkValidity();
+ assertFalse(certificateInfo.isValid());
+ }
+
+ @Test
+ void unsupportedCertificateTypeTest() {
+ final String certificateType = "unknown";
+ when(certificate.getType()).thenReturn(certificateType);
+ final CertificateInfoImpl certificateInfo = new CertificateInfoImpl(new File(""), certificate);
+ final UnsupportedOperationException actualException =
+ assertThrows(UnsupportedOperationException.class, certificateInfo::isValid);
+ assertEquals(actualException.getMessage(),
+ String.format("Certificate type '%s' not supported", certificateType));
+ }
+}