From b8c8f44f81eee778f6f038ae8dd171cc9f583741 Mon Sep 17 00:00:00 2001 From: Jim Hahn Date: Fri, 1 Nov 2019 15:13:10 -0400 Subject: Add REST api to query policy status from PAP As part of this, added code to pre-load the deployed policy tracker by reading the policies and groups from the DB. Change-Id: Ifc6c787d114a3a7add4ea54acc1cc969d6c3ca1c Issue-ID: POLICY-2024 Signed-off-by: Jim Hahn --- .../main/notification/PolicyCommonTrackerTest.java | 54 +++++++- .../pap/main/notification/PolicyNotifierTest.java | 146 ++++++++++++++++++++- .../policy/pap/main/rest/CommonPapRestServer.java | 23 +++- .../main/rest/TestPolicyStatusControllerV1.java | 76 +++++++++++ .../onap/policy/pap/main/rest/e2e/End2EndBase.java | 23 ++-- .../policy/pap/main/rest/e2e/PolicyStatusTest.java | 112 ++++++++++++++++ 6 files changed, 416 insertions(+), 18 deletions(-) create mode 100644 main/src/test/java/org/onap/policy/pap/main/rest/TestPolicyStatusControllerV1.java create mode 100644 main/src/test/java/org/onap/policy/pap/main/rest/e2e/PolicyStatusTest.java (limited to 'main/src/test/java/org') diff --git a/main/src/test/java/org/onap/policy/pap/main/notification/PolicyCommonTrackerTest.java b/main/src/test/java/org/onap/policy/pap/main/notification/PolicyCommonTrackerTest.java index 3318684c..e8c03d1d 100644 --- a/main/src/test/java/org/onap/policy/pap/main/notification/PolicyCommonTrackerTest.java +++ b/main/src/test/java/org/onap/policy/pap/main/notification/PolicyCommonTrackerTest.java @@ -30,6 +30,9 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; import org.junit.Before; import org.junit.Test; import org.onap.policy.models.pap.concepts.PolicyStatus; @@ -51,6 +54,54 @@ public class PolicyCommonTrackerTest extends PolicyCommonSupport { tracker = new MyTracker(); } + @Test + public void testGetStatus() { + tracker.addData(makeData(policy1, PDP1, PDP2)); + tracker.addData(makeData(policy2, PDP2)); + + List statusList = tracker.getStatus(); + assertEquals(2, statusList.size()); + + Set names = statusList.stream().map(PolicyStatus::getPolicyId).collect(Collectors.toSet()); + assertTrue(names.contains(policy1.getName())); + assertTrue(names.contains(policy2.getName())); + } + + @Test + public void testGetStatusString() { + tracker.addData(makeData(policy1, PDP1, PDP2)); + tracker.addData(makeData(policy2, PDP2)); + + policy3 = new ToscaPolicyIdentifier(policy1.getName(), policy1.getVersion() + "0"); + tracker.addData(makeData(policy3, PDP3)); + + List statusList = tracker.getStatus(policy1.getName()); + assertEquals(2, statusList.size()); + + Set idents = + statusList.stream().map(PolicyStatus::getPolicy).collect(Collectors.toSet()); + assertTrue(idents.contains(policy1)); + assertTrue(idents.contains(policy3)); + } + + @Test + public void testGetStatusToscaPolicyIdentifier() { + tracker.addData(makeData(policy1, PDP1, PDP2)); + tracker.addData(makeData(policy2, PDP2)); + + policy3 = new ToscaPolicyIdentifier(policy1.getName(), policy1.getVersion() + "0"); + tracker.addData(makeData(policy3, PDP3)); + + Optional status = tracker.getStatus(policy1); + assertTrue(status.isPresent()); + + assertEquals(policy1, status.get().getPolicy()); + + // check not-found case + status = tracker.getStatus(policy4); + assertFalse(status.isPresent()); + } + @Test public void testAddData() { tracker.addData(makeData(policy1, PDP1, PDP2)); @@ -113,7 +164,8 @@ public class PolicyCommonTrackerTest extends PolicyCommonSupport { } /** - * Tests removeData() when the subclass indicates that the policy should NOT be removed. + * Tests removeData() when the subclass indicates that the policy should NOT be + * removed. */ @Test public void testRemoveDataDoNotRemovePolicy() { diff --git a/main/src/test/java/org/onap/policy/pap/main/notification/PolicyNotifierTest.java b/main/src/test/java/org/onap/policy/pap/main/notification/PolicyNotifierTest.java index 1c65dd10..8c84337a 100644 --- a/main/src/test/java/org/onap/policy/pap/main/notification/PolicyNotifierTest.java +++ b/main/src/test/java/org/onap/policy/pap/main/notification/PolicyNotifierTest.java @@ -21,15 +21,22 @@ package org.onap.policy.pap.main.notification; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; import static org.mockito.Matchers.any; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; import java.util.List; +import java.util.Optional; import org.junit.Before; import org.junit.Test; import org.mockito.ArgumentCaptor; @@ -37,9 +44,18 @@ import org.mockito.Captor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.mockito.stubbing.Answer; +import org.onap.policy.models.base.PfModelException; import org.onap.policy.models.pap.concepts.PolicyNotification; import org.onap.policy.models.pap.concepts.PolicyStatus; +import org.onap.policy.models.pdp.concepts.Pdp; +import org.onap.policy.models.pdp.concepts.PdpGroup; +import org.onap.policy.models.pdp.concepts.PdpSubGroup; +import org.onap.policy.models.provider.PolicyModelsProvider; +import org.onap.policy.models.tosca.authorative.concepts.ToscaPolicy; import org.onap.policy.models.tosca.authorative.concepts.ToscaPolicyIdentifier; +import org.onap.policy.models.tosca.authorative.concepts.ToscaPolicyTypeIdentifier; +import org.onap.policy.pap.main.PolicyModelsProviderFactoryWrapper; +import org.onap.policy.pap.main.PolicyPapRuntimeException; import org.onap.policy.pap.main.comm.Publisher; import org.onap.policy.pap.main.comm.QueueToken; @@ -48,6 +64,12 @@ public class PolicyNotifierTest extends PolicyCommonSupport { @Mock private Publisher publisher; + @Mock + private PolicyModelsProviderFactoryWrapper daoFactory; + + @Mock + private PolicyModelsProvider dao; + @Mock private PolicyDeployTracker deploy; @@ -80,7 +102,123 @@ public class PolicyNotifierTest extends PolicyCommonSupport { super.setUp(); + try { + when(daoFactory.create()).thenReturn(dao); + when(dao.getPdpGroups(null)).thenReturn(Collections.emptyList()); + + notifier = new MyNotifier(publisher); + + } catch (PfModelException e) { + throw new PolicyPapRuntimeException(e); + } + } + + @Test + public void testLoadPoliciesPolicyModelsProviderFactoryWrapper() throws PfModelException { + final PdpGroup group1 = makeGroup("my group #1", makeSubGroup("sub #1 A", 2, policy1, policy4), + makeSubGroup("sub #1 B", 1, policy2)); + + // one policy is a duplicate + final PdpGroup group2 = makeGroup("my group #2", makeSubGroup("sub #2 A", 1, policy1, policy3)); + + when(dao.getPdpGroups(null)).thenReturn(Arrays.asList(group1, group2)); + + ToscaPolicyTypeIdentifier type2 = new ToscaPolicyTypeIdentifier("my other type", "8.8.8"); + + // note: no mapping for policy4 + when(dao.getFilteredPolicyList(any())).thenReturn(Arrays.asList(makePolicy(policy1, type), + makePolicy(policy2, type2), makePolicy(policy3, type))); + + // load it notifier = new MyNotifier(publisher); + + ArgumentCaptor captor = ArgumentCaptor.forClass(PolicyPdpNotificationData.class); + + // should have added policy1, policy2, policy1 (duplicate), policy3, but not + // policy4 + verify(deploy, times(4)).addData(captor.capture()); + + Iterator iter = captor.getAllValues().iterator(); + PolicyPdpNotificationData data = iter.next(); + assertEquals(policy1, data.getPolicyId()); + assertEquals(type, data.getPolicyType()); + assertEquals("[sub #1 A 0, sub #1 A 1]", data.getPdps().toString()); + + data = iter.next(); + assertEquals(policy2, data.getPolicyId()); + assertEquals(type2, data.getPolicyType()); + assertEquals("[sub #1 B 0]", data.getPdps().toString()); + + data = iter.next(); + assertEquals(policy1, data.getPolicyId()); + assertEquals(type, data.getPolicyType()); + assertEquals("[sub #2 A 0]", data.getPdps().toString()); + + data = iter.next(); + assertEquals(policy3, data.getPolicyId()); + assertEquals(type, data.getPolicyType()); + assertEquals("[sub #2 A 0]", data.getPdps().toString()); + } + + private ToscaPolicy makePolicy(ToscaPolicyIdentifier policyId, ToscaPolicyTypeIdentifier type) { + ToscaPolicy policy = new ToscaPolicy(); + + policy.setName(policyId.getName()); + policy.setVersion(policyId.getVersion()); + policy.setType(type.getName()); + policy.setTypeVersion(type.getVersion()); + + return policy; + } + + private PdpGroup makeGroup(String name, PdpSubGroup... subgrps) { + final PdpGroup group = new PdpGroup(); + group.setName(name); + + group.setPdpSubgroups(Arrays.asList(subgrps)); + + return group; + } + + private PdpSubGroup makeSubGroup(String name, int numPdps, ToscaPolicyIdentifier... policies) { + final PdpSubGroup subgrp = new PdpSubGroup(); + subgrp.setPdpType(name); + subgrp.setPdpInstances(new ArrayList<>(numPdps)); + + for (int x = 0; x < numPdps; ++x) { + Pdp pdp = new Pdp(); + pdp.setInstanceId(name + " " + x); + + subgrp.getPdpInstances().add(pdp); + } + + subgrp.setPolicies(Arrays.asList(policies)); + + return subgrp; + } + + @Test + public void testGetStatus() { + List statusList = Arrays.asList(status1); + when(deploy.getStatus()).thenReturn(statusList); + + assertSame(statusList, notifier.getStatus()); + } + + @Test + public void testGetStatusString() { + List statusList = Arrays.asList(status1); + when(deploy.getStatus("a policy")).thenReturn(statusList); + + assertSame(statusList, notifier.getStatus("a policy")); + } + + @Test + public void testGetStatusToscaPolicyIdentifier() { + Optional status = Optional.of(status1); + when(deploy.getStatus(policy1)).thenReturn(status); + + assertSame(status, notifier.getStatus(policy1)); } @Test @@ -161,9 +299,9 @@ public class PolicyNotifierTest extends PolicyCommonSupport { } @Test - public void testMakeDeploymentTracker_testMakeUndeploymentTracker() { + public void testMakeDeploymentTracker_testMakeUndeploymentTracker() throws PfModelException { // make real object, which will invoke the real makeXxx() methods - new PolicyNotifier(publisher).removePdp(PDP1); + new PolicyNotifier(publisher, daoFactory).removePdp(PDP1); verify(publisher, never()).enqueue(any()); } @@ -197,8 +335,8 @@ public class PolicyNotifierTest extends PolicyCommonSupport { private class MyNotifier extends PolicyNotifier { - public MyNotifier(Publisher publisher) { - super(publisher); + public MyNotifier(Publisher publisher) throws PfModelException { + super(publisher, daoFactory); } @Override diff --git a/main/src/test/java/org/onap/policy/pap/main/rest/CommonPapRestServer.java b/main/src/test/java/org/onap/policy/pap/main/rest/CommonPapRestServer.java index 91245084..8660d005 100644 --- a/main/src/test/java/org/onap/policy/pap/main/rest/CommonPapRestServer.java +++ b/main/src/test/java/org/onap/policy/pap/main/rest/CommonPapRestServer.java @@ -64,6 +64,8 @@ import org.slf4j.LoggerFactory; */ public class CommonPapRestServer { + protected static final String CONFIG_FILE = "src/test/resources/parameters/TestConfigParams.json"; + private static final Logger LOGGER = LoggerFactory.getLogger(CommonPapRestServer.class); private static String KEYSTORE = System.getProperty("user.dir") + "/src/test/resources/ssl/policy-keystore"; @@ -88,6 +90,17 @@ public class CommonPapRestServer { */ @BeforeClass public static void setUpBeforeClass() throws Exception { + setUpBeforeClass(true); + } + + /** + * Allocates a port for the server, writes a config file, and then starts Main, if + * specified. + * + * @param shouldStart {@code true} if Main should be started, {@code false} otherwise + * @throws Exception if an error occurs + */ + public static void setUpBeforeClass(boolean shouldStart) throws Exception { port = NetworkUtil.allocPort(); httpsPrefix = "https://localhost:" + port + "/"; @@ -99,7 +112,9 @@ public class CommonPapRestServer { CommonTestData.newDb(); - startMain(); + if (shouldStart) { + startMain(); + } } /** @@ -159,7 +174,7 @@ public class CommonPapRestServer { private static void makeConfigFile() throws Exception { String json = new CommonTestData().getPapParameterGroupAsString(port); - File file = new File("src/test/resources/parameters/TestConfigParams.json"); + File file = new File(CONFIG_FILE); file.deleteOnExit(); try (FileOutputStream output = new FileOutputStream(file)) { @@ -172,7 +187,7 @@ public class CommonPapRestServer { * * @throws Exception if an error occurs */ - private static void startMain() throws Exception { + protected static void startMain() throws Exception { Registry.newRegistry(); // make sure port is available @@ -185,7 +200,7 @@ public class CommonPapRestServer { systemProps.put("javax.net.ssl.keyStorePassword", "Pol1cy_0nap"); System.setProperties(systemProps); - final String[] papConfigParameters = { "-c", "src/test/resources/parameters/TestConfigParams.json" }; + final String[] papConfigParameters = { "-c", CONFIG_FILE }; main = new Main(papConfigParameters); diff --git a/main/src/test/java/org/onap/policy/pap/main/rest/TestPolicyStatusControllerV1.java b/main/src/test/java/org/onap/policy/pap/main/rest/TestPolicyStatusControllerV1.java new file mode 100644 index 00000000..1f7c6d0f --- /dev/null +++ b/main/src/test/java/org/onap/policy/pap/main/rest/TestPolicyStatusControllerV1.java @@ -0,0 +1,76 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2019 Nordix Foundation. + * Modifications Copyright (C) 2019 AT&T Intellectual Property. + * ================================================================================ + * 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.policy.pap.main.rest; + +import static org.junit.Assert.assertEquals; + +import javax.ws.rs.client.Invocation; +import javax.ws.rs.core.Response; +import org.junit.Test; + +/** + * Note: this tests failure cases; success cases are tested by tests in the "e2e" package. + */ +public class TestPolicyStatusControllerV1 extends CommonPapRestServer { + + private static final String POLICY_STATUS_ENDPOINT = "policies/deployed"; + + @Test + public void testSwagger() throws Exception { + super.testSwagger(POLICY_STATUS_ENDPOINT); + + super.testSwagger(POLICY_STATUS_ENDPOINT + "/{name}"); + super.testSwagger(POLICY_STATUS_ENDPOINT + "/{name}/{version}"); + } + + @Test + public void queryAllDeployedPolicies() throws Exception { + String uri = POLICY_STATUS_ENDPOINT; + + // verify it fails when no authorization info is included + checkUnauthRequest(uri, req -> req.get()); + } + + @Test + public void testQueryDeployedPolicies() throws Exception { + String uri = POLICY_STATUS_ENDPOINT + "/my-name"; + + Invocation.Builder invocationBuilder = sendRequest(uri); + Response rawresp = invocationBuilder.get(); + assertEquals(Response.Status.NOT_FOUND.getStatusCode(), rawresp.getStatus()); + + // verify it fails when no authorization info is included + checkUnauthRequest(uri, req -> req.get()); + } + + @Test + public void queryDeployedPolicy() throws Exception { + String uri = POLICY_STATUS_ENDPOINT + "/my-name/1.2.3"; + + Invocation.Builder invocationBuilder = sendRequest(uri); + Response rawresp = invocationBuilder.get(); + assertEquals(Response.Status.NOT_FOUND.getStatusCode(), rawresp.getStatus()); + + // verify it fails when no authorization info is included + checkUnauthRequest(uri, req -> req.get()); + } +} diff --git a/main/src/test/java/org/onap/policy/pap/main/rest/e2e/End2EndBase.java b/main/src/test/java/org/onap/policy/pap/main/rest/e2e/End2EndBase.java index 7e217423..10c791ea 100644 --- a/main/src/test/java/org/onap/policy/pap/main/rest/e2e/End2EndBase.java +++ b/main/src/test/java/org/onap/policy/pap/main/rest/e2e/End2EndBase.java @@ -32,14 +32,13 @@ import org.onap.policy.common.utils.coder.Coder; import org.onap.policy.common.utils.coder.CoderException; import org.onap.policy.common.utils.coder.StandardCoder; import org.onap.policy.common.utils.resources.ResourceUtils; -import org.onap.policy.common.utils.services.Registry; import org.onap.policy.models.base.PfModelException; import org.onap.policy.models.pdp.concepts.PdpGroups; import org.onap.policy.models.provider.PolicyModelsProvider; import org.onap.policy.models.tosca.authorative.concepts.ToscaServiceTemplate; -import org.onap.policy.pap.main.PapConstants; import org.onap.policy.pap.main.PolicyModelsProviderFactoryWrapper; import org.onap.policy.pap.main.PolicyPapRuntimeException; +import org.onap.policy.pap.main.parameters.PapParameterGroup; import org.onap.policy.pap.main.rest.CommonPapRestServer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -75,15 +74,21 @@ public class End2EndBase extends CommonPapRestServer { */ @BeforeClass public static void setUpBeforeClass() throws Exception { - CommonPapRestServer.setUpBeforeClass(); + setUpBeforeClass(true); + } - daoFactory = Registry.get(PapConstants.REG_PAP_DAO_FACTORY, PolicyModelsProviderFactoryWrapper.class); + /** + * Starts Main, if specified, and connects to the DB. + * + * @param shouldStart {@code true} if Main should be started, {@code false} otherwise + * @throws Exception if an error occurs + */ + public static void setUpBeforeClass(boolean shouldStart) throws Exception { + CommonPapRestServer.setUpBeforeClass(shouldStart); - try { - dbConn = daoFactory.create(); - } catch (PfModelException e) { - throw new PolicyPapRuntimeException("cannot connect to DB", e); - } + PapParameterGroup params = new StandardCoder().decode(new File(CONFIG_FILE), PapParameterGroup.class); + daoFactory = new PolicyModelsProviderFactoryWrapper(params.getDatabaseProviderParameters()); + dbConn = daoFactory.create(); } /** diff --git a/main/src/test/java/org/onap/policy/pap/main/rest/e2e/PolicyStatusTest.java b/main/src/test/java/org/onap/policy/pap/main/rest/e2e/PolicyStatusTest.java new file mode 100644 index 00000000..afabb892 --- /dev/null +++ b/main/src/test/java/org/onap/policy/pap/main/rest/e2e/PolicyStatusTest.java @@ -0,0 +1,112 @@ +/* + * ============LICENSE_START======================================================= + * ONAP PAP + * ================================================================================ + * Copyright (C) 2019 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.pap.main.rest.e2e; + +import static org.junit.Assert.assertEquals; + +import java.util.List; +import javax.ws.rs.client.Invocation; +import javax.ws.rs.core.GenericType; +import javax.ws.rs.core.Response; +import org.junit.BeforeClass; +import org.junit.Test; +import org.onap.policy.models.pap.concepts.PolicyStatus; + +public class PolicyStatusTest extends End2EndBase { + private static final String POLICY_STATUS_ENDPOINT = "policies/deployed"; + + /** + * Starts Main and adds policies to the DB. + * + * @throws Exception if an error occurs + */ + @BeforeClass + public static void setUpBeforeClass() throws Exception { + // don't start Main until AFTER we add the policies to the DB + End2EndBase.setUpBeforeClass(false); + + addToscaPolicyTypes("monitoring.policy-type.yaml"); + addToscaPolicies("monitoring.policy.yaml"); + addGroups("policyStatus.json"); + + startMain(); + } + + @Test + public void testQueryAllDeployedPolicies() throws Exception { + String uri = POLICY_STATUS_ENDPOINT; + + Invocation.Builder invocationBuilder = sendRequest(uri); + Response rawresp = invocationBuilder.get(); + assertEquals(Response.Status.OK.getStatusCode(), rawresp.getStatus()); + + List resp = rawresp.readEntity(new GenericType>() {}); + assertEquals(1, resp.size()); + + PolicyStatus status = resp.get(0); + assertEquals("onap.restart.tca", status.getPolicyId()); + assertEquals("1.0.0", status.getPolicyVersion()); + assertEquals("onap.policies.monitoring.cdap.tca.hi.lo.app", status.getPolicyTypeId()); + assertEquals("1.0.0", status.getPolicyTypeVersion()); + assertEquals(0, status.getFailureCount()); + assertEquals(1, status.getIncompleteCount()); + assertEquals(0, status.getSuccessCount()); + } + + @Test + public void testQueryDeployedPolicies() throws Exception { + String uri = POLICY_STATUS_ENDPOINT + "/onap.restart.tca"; + + Invocation.Builder invocationBuilder = sendRequest(uri); + Response rawresp = invocationBuilder.get(); + assertEquals(Response.Status.OK.getStatusCode(), rawresp.getStatus()); + + List resp = rawresp.readEntity(new GenericType>() {}); + assertEquals(1, resp.size()); + + PolicyStatus status = resp.get(0); + assertEquals("onap.restart.tca", status.getPolicyId()); + assertEquals("1.0.0", status.getPolicyVersion()); + assertEquals("onap.policies.monitoring.cdap.tca.hi.lo.app", status.getPolicyTypeId()); + assertEquals("1.0.0", status.getPolicyTypeVersion()); + assertEquals(0, status.getFailureCount()); + assertEquals(1, status.getIncompleteCount()); + assertEquals(0, status.getSuccessCount()); + } + + @Test + public void testQueryDeployedPolicy() throws Exception { + String uri = POLICY_STATUS_ENDPOINT + "/onap.restart.tca/1.0.0"; + + Invocation.Builder invocationBuilder = sendRequest(uri); + Response rawresp = invocationBuilder.get(); + assertEquals(Response.Status.OK.getStatusCode(), rawresp.getStatus()); + + PolicyStatus status = rawresp.readEntity(PolicyStatus.class); + assertEquals("onap.restart.tca", status.getPolicyId()); + assertEquals("1.0.0", status.getPolicyVersion()); + assertEquals("onap.policies.monitoring.cdap.tca.hi.lo.app", status.getPolicyTypeId()); + assertEquals("1.0.0", status.getPolicyTypeVersion()); + assertEquals(0, status.getFailureCount()); + assertEquals(1, status.getIncompleteCount()); + assertEquals(0, status.getSuccessCount()); + } +} -- cgit 1.2.3-korg