From 9ac6c3cc7e3f4aec9ba69e7a5bb0800471c4e8dc Mon Sep 17 00:00:00 2001 From: MatthieuGeerebaert Date: Wed, 28 Mar 2018 17:12:51 +0200 Subject: Add service inventory - Add service inventory operations get and find - Consumes AAI - Add tests Change-Id: If04ada259b7a172c1dbaac3649047cdb2d9dd5bc Issue-ID: EXTAPI-39 Signed-off-by: MatthieuGeerebaert --- .../java/org/onap/nbi/OnapComponentsUrlPaths.java | 2 +- .../onap/nbi/apis/servicecatalog/SdcClient.java | 5 +- .../onap/nbi/apis/serviceinventory/AaiClient.java | 82 ++++++++++++ .../onap/nbi/apis/serviceinventory/BaseClient.java | 33 +++++ .../onap/nbi/apis/serviceinventory/NbiClient.java | 20 +++ .../serviceinventory/ServiceInventoryResource.java | 45 +++++++ .../serviceinventory/ServiceInventoryService.java | 149 +++++++++++++++++++++ .../jolt/FindServiceInventoryJsonTransformer.java | 35 +++++ .../jolt/GetServiceInventoryJsonTransformer.java | 36 +++++ .../resources/application-localhost.properties | 16 +++ src/main/resources/jolt/findServiceInventory.json | 24 ++++ src/main/resources/jolt/getServiceInventory.json | 44 ++++++ 12 files changed, 487 insertions(+), 4 deletions(-) create mode 100644 src/main/java/org/onap/nbi/apis/serviceinventory/AaiClient.java create mode 100644 src/main/java/org/onap/nbi/apis/serviceinventory/BaseClient.java create mode 100644 src/main/java/org/onap/nbi/apis/serviceinventory/NbiClient.java create mode 100644 src/main/java/org/onap/nbi/apis/serviceinventory/ServiceInventoryResource.java create mode 100644 src/main/java/org/onap/nbi/apis/serviceinventory/ServiceInventoryService.java create mode 100644 src/main/java/org/onap/nbi/apis/serviceinventory/jolt/FindServiceInventoryJsonTransformer.java create mode 100644 src/main/java/org/onap/nbi/apis/serviceinventory/jolt/GetServiceInventoryJsonTransformer.java create mode 100644 src/main/resources/jolt/findServiceInventory.json create mode 100644 src/main/resources/jolt/getServiceInventory.json (limited to 'src/main') diff --git a/src/main/java/org/onap/nbi/OnapComponentsUrlPaths.java b/src/main/java/org/onap/nbi/OnapComponentsUrlPaths.java index 5ee6528..797fde8 100644 --- a/src/main/java/org/onap/nbi/OnapComponentsUrlPaths.java +++ b/src/main/java/org/onap/nbi/OnapComponentsUrlPaths.java @@ -15,7 +15,7 @@ public final class OnapComponentsUrlPaths { // AAI public static final String AAI_GET_TENANTS_PATH = - "/aai/v11/cloud-infrastructure/cloud-regions/cloud-region/$cloudOwner/$lcpCloudRegionId/tenants"; + "/aai/v11/cloud-infrastructure/cloud-regions/cloud-region/$onap.cloudOwner/$onap.lcpCloudRegionId/tenants"; public static final String AAI_GET_CUSTOMER_PATH = "/aai/v11/business/customers/customer/"; public static final String AAI_GET_SERVICES_FOR_CUSTOMER_PATH = "/aai/v11/business/customers/customer/$customerId/service-subscriptions"; diff --git a/src/main/java/org/onap/nbi/apis/servicecatalog/SdcClient.java b/src/main/java/org/onap/nbi/apis/servicecatalog/SdcClient.java index abc8bda..5bab982 100644 --- a/src/main/java/org/onap/nbi/apis/servicecatalog/SdcClient.java +++ b/src/main/java/org/onap/nbi/apis/servicecatalog/SdcClient.java @@ -50,8 +50,8 @@ public class SdcClient { public LinkedHashMap callGet(String id) { - StringBuilder callURL = new StringBuilder().append(sdcHost).append(OnapComponentsUrlPaths.SDC_ROOT_URL).append(id) - .append(OnapComponentsUrlPaths.SDC_GET_PATH); + StringBuilder callURL = new StringBuilder().append(sdcHost).append(OnapComponentsUrlPaths.SDC_ROOT_URL) + .append(id).append(OnapComponentsUrlPaths.SDC_GET_PATH); ResponseEntity response = callSdc(callURL.toString()); return (LinkedHashMap) response.getBody(); @@ -134,7 +134,6 @@ public class SdcClient { } catch (BackendFunctionalException e) { LOGGER.error(HTTP_CALL_SDC_ON + callURL.toString() + " error " + e); - return null; } } diff --git a/src/main/java/org/onap/nbi/apis/serviceinventory/AaiClient.java b/src/main/java/org/onap/nbi/apis/serviceinventory/AaiClient.java new file mode 100644 index 0000000..1f5f74d --- /dev/null +++ b/src/main/java/org/onap/nbi/apis/serviceinventory/AaiClient.java @@ -0,0 +1,82 @@ +package org.onap.nbi.apis.serviceinventory; + +import java.util.LinkedHashMap; +import org.onap.nbi.OnapComponentsUrlPaths; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; + +@Service +public class AaiClient extends BaseClient { + + public static final String CUSTOMER_ID = "$customerId"; + + @Value("${aai.host}") + private String aaiHost; + + @Value("${aai.header.authorization}") + private String aaiHeaderAuthorization; + + @Value("${aai.api.id}") + private String aaiApiId; + + private static final String HEADER_AUTHORIZATION = "Authorization"; + private static final String X_FROM_APP_ID = "X-FromAppId"; + + private HttpHeaders buildRequestHeaderForAAI() { + + HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.add(HEADER_AUTHORIZATION, aaiHeaderAuthorization); + httpHeaders.add(X_FROM_APP_ID, aaiApiId); + httpHeaders.add("Accept", "application/json"); + httpHeaders.add("Content-Type", "application/json"); + return httpHeaders; + + } + + public LinkedHashMap getCatalogService(String customerId, String serviceSpecName, String serviceId) { + + StringBuilder callURL = + new StringBuilder().append(aaiHost).append(OnapComponentsUrlPaths.AAI_GET_SERVICE_FOR_CUSTOMER_PATH); + String callUrlFormated = callURL.toString().replace(CUSTOMER_ID, customerId); + callUrlFormated = callUrlFormated.replace("$serviceSpecName", serviceSpecName); + callUrlFormated = callUrlFormated.replace("$serviceId", serviceId); + + ResponseEntity response = callApiGet(callUrlFormated, buildRequestHeaderForAAI()); + if (response != null && response.getStatusCode().equals(HttpStatus.OK)) { + return (LinkedHashMap) response.getBody(); + } + return null; + } + + + public LinkedHashMap getVNF(String relatedLink) { + + StringBuilder callURL = new StringBuilder().append(aaiHost).append(relatedLink); + + ResponseEntity response = callApiGet(callURL.toString(), buildRequestHeaderForAAI()); + return (LinkedHashMap) response.getBody(); + + } + + public LinkedHashMap getServicesInAaiForCustomer(String customerId) { + StringBuilder callURL = + new StringBuilder().append(aaiHost).append(OnapComponentsUrlPaths.AAI_GET_SERVICES_FOR_CUSTOMER_PATH); + String callUrlFormated = callURL.toString().replace(CUSTOMER_ID, customerId); + + ResponseEntity response = callApiGet(callUrlFormated, buildRequestHeaderForAAI()); + return (LinkedHashMap) response.getBody(); + } + + public LinkedHashMap getServiceInstancesInAaiForCustomer(String customerId, String serviceType) { + StringBuilder callURL = + new StringBuilder().append(aaiHost).append(OnapComponentsUrlPaths.AAI_GET_SERVICE_INSTANCES_PATH); + String callUrlFormated = callURL.toString().replace(CUSTOMER_ID, customerId); + callUrlFormated = callUrlFormated.replace("$serviceSpecName", serviceType); + + ResponseEntity response = callApiGet(callUrlFormated, buildRequestHeaderForAAI()); + return (LinkedHashMap) response.getBody(); + } +} diff --git a/src/main/java/org/onap/nbi/apis/serviceinventory/BaseClient.java b/src/main/java/org/onap/nbi/apis/serviceinventory/BaseClient.java new file mode 100644 index 0000000..b9a93d0 --- /dev/null +++ b/src/main/java/org/onap/nbi/apis/serviceinventory/BaseClient.java @@ -0,0 +1,33 @@ +package org.onap.nbi.apis.serviceinventory; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.client.RestTemplate; + +public abstract class BaseClient { + + private static final Logger LOGGER = LoggerFactory.getLogger(BaseClient.class); + + @Autowired + private RestTemplate restTemplate; + + protected ResponseEntity callApiGet(String callURL, HttpHeaders httpHeaders) { + + ResponseEntity response = restTemplate.exchange(callURL, HttpMethod.GET, + new HttpEntity<>("parameters", httpHeaders), Object.class); + LOGGER.debug("response body : " + response.getBody().toString()); + LOGGER.info("response status : " + response.getStatusCodeValue()); + if (!response.getStatusCode().equals(HttpStatus.OK)) { + LOGGER.warn("HTTP call on " + callURL + " returns " + response.getStatusCodeValue() + ", " + + response.getBody().toString()); + } + return response; + } + +} diff --git a/src/main/java/org/onap/nbi/apis/serviceinventory/NbiClient.java b/src/main/java/org/onap/nbi/apis/serviceinventory/NbiClient.java new file mode 100644 index 0000000..cbbd88d --- /dev/null +++ b/src/main/java/org/onap/nbi/apis/serviceinventory/NbiClient.java @@ -0,0 +1,20 @@ +package org.onap.nbi.apis.serviceinventory; + +import java.util.LinkedHashMap; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpHeaders; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; + +@Service +public class NbiClient extends BaseClient { + + @Value("${nbi.url}") + private String nbiUrl; + + public LinkedHashMap getServiceSpecification(String id) { + StringBuilder callURL = new StringBuilder().append(nbiUrl).append("/serviceSpecification/").append(id); + ResponseEntity response = callApiGet(callURL.toString(), new HttpHeaders()); + return (LinkedHashMap) response.getBody(); + } +} diff --git a/src/main/java/org/onap/nbi/apis/serviceinventory/ServiceInventoryResource.java b/src/main/java/org/onap/nbi/apis/serviceinventory/ServiceInventoryResource.java new file mode 100644 index 0000000..9092381 --- /dev/null +++ b/src/main/java/org/onap/nbi/apis/serviceinventory/ServiceInventoryResource.java @@ -0,0 +1,45 @@ +package org.onap.nbi.apis.serviceinventory; + +import java.util.LinkedHashMap; +import java.util.List; +import org.onap.nbi.commons.JsonRepresentation; +import org.onap.nbi.commons.ResourceManagement; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.util.MultiValueMap; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/service") +public class ServiceInventoryResource extends ResourceManagement { + + @Autowired + ServiceInventoryService serviceInventoryService; + + @GetMapping(value = "/{serviceId}", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity getServiceInventory(@PathVariable String serviceId, + @RequestParam MultiValueMap params) { + + LinkedHashMap response = serviceInventoryService.get(serviceId, params); + + JsonRepresentation filter = new JsonRepresentation(params); + return this.getResponse(response, filter); + + } + + @GetMapping(value = "", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity findServiceInventory(@RequestParam MultiValueMap params) { + + List response = serviceInventoryService.find(params); + JsonRepresentation filter = new JsonRepresentation(params); + return this.findResponse(response, filter, null); + + } + + +} diff --git a/src/main/java/org/onap/nbi/apis/serviceinventory/ServiceInventoryService.java b/src/main/java/org/onap/nbi/apis/serviceinventory/ServiceInventoryService.java new file mode 100644 index 0000000..886a464 --- /dev/null +++ b/src/main/java/org/onap/nbi/apis/serviceinventory/ServiceInventoryService.java @@ -0,0 +1,149 @@ +package org.onap.nbi.apis.serviceinventory; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import org.onap.nbi.apis.serviceinventory.jolt.FindServiceInventoryJsonTransformer; +import org.onap.nbi.apis.serviceinventory.jolt.GetServiceInventoryJsonTransformer; +import org.onap.nbi.exceptions.BackendFunctionalException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; +import org.springframework.util.MultiValueMap; +import org.springframework.util.StringUtils; + +@Service +public class ServiceInventoryService { + + @Autowired + NbiClient nbiClient; + + @Autowired + AaiClient aaiClient; + + @Autowired + GetServiceInventoryJsonTransformer getServiceInventoryJsonTransformer; + + @Autowired + FindServiceInventoryJsonTransformer findServiceInventoryJsonTransformer; + + public LinkedHashMap get(String serviceId, MultiValueMap params) { + + String clientId = params.getFirst("relatedParty.id"); + String serviceSpecId = params.getFirst("serviceSpecification.id"); + String serviceSpecName = params.getFirst("serviceSpecification.name"); + + if (StringUtils.isEmpty(serviceSpecId) && StringUtils.isEmpty(serviceSpecName)) { + throw new BackendFunctionalException(HttpStatus.NOT_FOUND, + "serviceSpecName or serviceSpecId must be provided"); + } + + String customerId = getCustomerId(clientId); + String serviceName = getServiceName(serviceSpecName, serviceSpecId); + LinkedHashMap serviceResponse = aaiClient.getCatalogService(customerId, serviceName, serviceId); + + if (serviceResponse != null) { + addVnfsToResponse(serviceResponse); + LinkedHashMap serviceInventoryResponse = + (LinkedHashMap) getServiceInventoryJsonTransformer.transform(serviceResponse); + addRelatedPartyId(customerId, serviceInventoryResponse); + return serviceInventoryResponse; + } else { + throw new BackendFunctionalException(HttpStatus.NOT_FOUND, "no catalog service found"); + } + + } + + + private String getCustomerId(String clientId) { + + if (StringUtils.isEmpty(clientId)) { + return "generic"; + } else { + return clientId; + } + + } + + private String getServiceName(String serviceSpecificationName, String serviceSpecificationId) { + + if (StringUtils.isEmpty(serviceSpecificationName)) { + LinkedHashMap serviceSpecification = nbiClient.getServiceSpecification(serviceSpecificationId); + return (String) serviceSpecification.get("name"); + } else { + return serviceSpecificationName; + } + + } + + private void addRelatedPartyId(String customerId, LinkedHashMap serviceInventoryResponse) { + + LinkedHashMap relatedParty = (LinkedHashMap) serviceInventoryResponse.get("relatedParty"); + relatedParty.put("id", customerId); + + } + + private void addVnfsToResponse(LinkedHashMap serviceResponse) { + + List vnfs = new ArrayList<>(); + LinkedHashMap relationShip = (LinkedHashMap) serviceResponse.get("relationship-list"); + List relationsList = (List) relationShip.get("relationship"); + for (LinkedHashMap relation : relationsList) { + String relatedLink = (String) relation.get("related-link"); + LinkedHashMap vnf = aaiClient.getVNF(relatedLink); + if (vnf != null) { + vnfs.add(vnf); + } + } + serviceResponse.put("vnfs", vnfs); + + } + + + public List find(MultiValueMap params) { + + String clientId = params.getFirst("relatedParty.id"); + String serviceSpecId = params.getFirst("serviceSpecification.id"); + String serviceSpecName = params.getFirst("serviceSpecification.name"); + String customerId = getCustomerId(clientId); + String serviceName; + List serviceInstances = new ArrayList<>(); + if (StringUtils.isEmpty(serviceSpecId) && StringUtils.isEmpty(serviceSpecName)) { + LinkedHashMap servicesInAaiForCustomer = aaiClient.getServicesInAaiForCustomer(customerId); + List servicesInAAI = + (List) servicesInAaiForCustomer.get("service-subscription"); + for (LinkedHashMap service : servicesInAAI) { + String serviceType = (String) service.get("service-type"); + buildServiceInstances(serviceInstances, customerId, serviceType); + } + } else { + serviceName = getServiceName(serviceSpecName, serviceSpecId); + buildServiceInstances(serviceInstances, customerId, serviceName); + } + + List serviceInventoryResponse = + (List) findServiceInventoryJsonTransformer.transform(serviceInstances); + for (LinkedHashMap serviceInventory : serviceInventoryResponse) { + LinkedHashMap party = (LinkedHashMap) serviceInventory.get("relatedParty"); + party.put("id", customerId); + } + return serviceInventoryResponse; + + } + + private void buildServiceInstances(List serviceInstances, String customerId, String serviceType) { + + LinkedHashMap serviceInstancesInAaiForCustomer = + aaiClient.getServiceInstancesInAaiForCustomer(customerId, serviceType); + List serviceInstancesForServiceType = + (List) serviceInstancesInAaiForCustomer.get("service-instance"); + + // add service type for jolt + for (LinkedHashMap serviceInstanceForServiceType : serviceInstancesForServiceType) { + serviceInstanceForServiceType.put("service-type", serviceType); + } + serviceInstances.addAll(serviceInstancesForServiceType); + + } + +} diff --git a/src/main/java/org/onap/nbi/apis/serviceinventory/jolt/FindServiceInventoryJsonTransformer.java b/src/main/java/org/onap/nbi/apis/serviceinventory/jolt/FindServiceInventoryJsonTransformer.java new file mode 100644 index 0000000..8173215 --- /dev/null +++ b/src/main/java/org/onap/nbi/apis/serviceinventory/jolt/FindServiceInventoryJsonTransformer.java @@ -0,0 +1,35 @@ +package org.onap.nbi.apis.serviceinventory.jolt; + +import java.util.List; +import org.onap.nbi.exceptions.TechnicalException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; +import com.bazaarvoice.jolt.Chainr; +import com.bazaarvoice.jolt.JsonUtils; +import com.bazaarvoice.jolt.exception.JoltException; + +@Service +public class FindServiceInventoryJsonTransformer { + + private Chainr chainr; + + private static final Logger LOGGER = LoggerFactory.getLogger(FindServiceInventoryJsonTransformer.class); + + public FindServiceInventoryJsonTransformer() { + List specs = JsonUtils.classpathToList("/jolt/findServiceInventory.json"); + this.chainr = Chainr.fromSpec(specs); + } + + public Object transform(Object serviceSpec) { + Object output = null; + try { + output = chainr.transform(serviceSpec); + } catch (JoltException joE) { + LOGGER.error("Unable to transform SDC response with JOLT Transformer : " + joE.getMessage()); + throw new TechnicalException("Error while parsing ONAP response"); + } + return output; + } + +} diff --git a/src/main/java/org/onap/nbi/apis/serviceinventory/jolt/GetServiceInventoryJsonTransformer.java b/src/main/java/org/onap/nbi/apis/serviceinventory/jolt/GetServiceInventoryJsonTransformer.java new file mode 100644 index 0000000..f82c696 --- /dev/null +++ b/src/main/java/org/onap/nbi/apis/serviceinventory/jolt/GetServiceInventoryJsonTransformer.java @@ -0,0 +1,36 @@ +package org.onap.nbi.apis.serviceinventory.jolt; + +import java.util.List; +import org.onap.nbi.exceptions.TechnicalException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; +import com.bazaarvoice.jolt.Chainr; +import com.bazaarvoice.jolt.JsonUtils; +import com.bazaarvoice.jolt.exception.JoltException; + +@Service +public class GetServiceInventoryJsonTransformer { + + private Chainr chainr; + + private static final Logger LOGGER = LoggerFactory.getLogger(GetServiceInventoryJsonTransformer.class); + + + public GetServiceInventoryJsonTransformer() { + List specs = JsonUtils.classpathToList("/jolt/getServiceInventory.json"); + this.chainr = Chainr.fromSpec(specs); + } + + public Object transform(Object serviceSpec) { + Object output = null; + try { + output = chainr.transform(serviceSpec); + } catch (JoltException joE) { + LOGGER.error("Unable to transform SDC response with JOLT Transformer : " + joE.getMessage()); + throw new TechnicalException("Error while parsing ONAP response"); + } + return output; + } + +} diff --git a/src/main/resources/application-localhost.properties b/src/main/resources/application-localhost.properties index 58b13f6..e753488 100644 --- a/src/main/resources/application-localhost.properties +++ b/src/main/resources/application-localhost.properties @@ -1,7 +1,23 @@ # LOGGING logging.level.org.onap.nbi=DEBUG +# ONAP +onap.lcpCloudRegionId=RegionOne +onap.tenantId=31047205ce114b60833b23e400d6a535 +onap.cloudOwner=CloudOwner + +# NBI +nbi.url=http://127.0.0.1:8080/nbi/api/v1 + # SDC sdc.host=http://127.0.0.1:8090 sdc.header.ecompInstanceId=Rene sdc.header.authorization=Basic YWFpOktwOGJKNFNYc3pNMFdYbGhhazNlSGxjc2UyZ0F3ODR2YW9HR21KdlV5MlU= + +# AAI +aai.host=http://127.0.0.1:8090 +aai.header.authorization=Basic QUFJOkFBSQ== +aai.api.id=AAI + + + diff --git a/src/main/resources/jolt/findServiceInventory.json b/src/main/resources/jolt/findServiceInventory.json new file mode 100644 index 0000000..7188edc --- /dev/null +++ b/src/main/resources/jolt/findServiceInventory.json @@ -0,0 +1,24 @@ +[ + { + "operation": "shift", + "spec": { + "*": { + "service-instance-id": "[&1].id", + "service-instance-name": "[&1].name", + "service-type": "[&1].serviceSpecification.name", + "model-version-id": "[&1].serviceSpecification.id" + } + } + }, + { + "operation": "default", + "spec": { + "*": { + "relatedParty": { + + "role": "ONAPcustomer" + } + } + } + } +] \ No newline at end of file diff --git a/src/main/resources/jolt/getServiceInventory.json b/src/main/resources/jolt/getServiceInventory.json new file mode 100644 index 0000000..df4f14c --- /dev/null +++ b/src/main/resources/jolt/getServiceInventory.json @@ -0,0 +1,44 @@ +[ + { + "operation": "shift", + "spec": { + "service-instance-id": "id", + "service-instance-name": "name", + "model-version-id" : "serviceSpecification.id", + "model-invariant-id": "serviceSpecification.invariantUUID", + "vnfs" : { + "*": { + "vnf-id": "supportingResource[&1].id", + "related-link": "supportingResource[&1].href", + "vnf-name": "supportingResource[&1].name", + "prov-status": "supportingResource[&1].status", + "model-invariant-id": "supportingResource[&1].modelInvariantId", + "model-version-id": "supportingResource[&1].modelVersionId", + "model-customisation-id": "supportingResource[&1].status" + + } + } + } + }, + { + "operation": "default", + "spec": { + "type": "service-instance", + "hasStarted": "yes", + "@type": "serviceONAP", + "serviceSpecification" : { + "@type" :"ONAPservice" + }, + "supportingResource[]" : { + "*": { + "@referredType": "ONAP resource" + } + + }, + "relatedParty" : { + "role" :"ONAPcustomer" + } + + } + } +] \ No newline at end of file -- cgit 1.2.3-korg