NONRTRIC-1005: Support for regex capture groups

Issue-ID: NONRTRIC-1005
Change-Id: I4f32d68649c840701a37429d1c956f9b45bcc603
Signed-off-by: DenisGNoonan <denis.noonan@est.tech>
diff --git a/servicemanager/.env.example b/servicemanager/.env.example
index 1d3c4ef..c5d5b32 100644
--- a/servicemanager/.env.example
+++ b/servicemanager/.env.example
@@ -8,7 +8,7 @@
 KONG_DATA_PLANE_IPV4=<host string>
 KONG_DATA_PLANE_PORT=<port number>
 CAPIF_PROTOCOL=<http or https protocol scheme>
-CAPIF_IPV4=<host>
+CAPIF_IPV4=<host string>
 CAPIF_PORT=<port number>
 LOG_LEVEL=<Trace, Debug, Info, Warning, Error, Fatal or Panic>
 SERVICE_MANAGER_PORT=<port number>
diff --git a/servicemanager/internal/envreader/envreader.go b/servicemanager/internal/envreader/envreader.go
index f08b1d7..315772e 100644
--- a/servicemanager/internal/envreader/envreader.go
+++ b/servicemanager/internal/envreader/envreader.go
@@ -21,6 +21,8 @@
 package envreader
 
 import (
+	"fmt"
+	"net/url"
 	"os"
 	"path/filepath"
 	"runtime"
@@ -75,6 +77,14 @@
 	logConfig(myEnv, envFile)
 
 	myPorts, err := createMapPorts(myEnv)
+	if err == nil {
+		err = validateEnv(myEnv)
+	}
+
+	if err == nil {
+		err = validateUrls(myEnv, myPorts)
+	}
+
 	return myEnv, myPorts, err
 }
 
@@ -110,31 +120,98 @@
 	log.Infof("TEST_SERVICE_PORT %s", myEnv["TEST_SERVICE_PORT"])
 }
 
+func validateUrls(myEnv map[string]string, myPorts map[string]int) error {
+	capifProtocol := myEnv["CAPIF_PROTOCOL"]
+	capifIPv4 := myEnv["CAPIF_IPV4"]
+	capifPort := myPorts["CAPIF_PORT"]
+	capifcoreUrl := fmt.Sprintf("%s://%s:%d", capifProtocol, capifIPv4, capifPort)
+
+	kongProtocol := myEnv["KONG_PROTOCOL"]
+	kongControlPlaneIPv4 := myEnv["KONG_CONTROL_PLANE_IPV4"]
+	kongControlPlanePort := myPorts["KONG_CONTROL_PLANE_PORT"]
+	kongControlPlaneURL := fmt.Sprintf("%s://%s:%d", kongProtocol, kongControlPlaneIPv4, kongControlPlanePort)
+
+	kongDataPlaneIPv4 := myEnv["KONG_DATA_PLANE_IPV4"]
+	kongDataPlanePort := myPorts["KONG_DATA_PLANE_PORT"]
+	kongDataPlaneURL := fmt.Sprintf("%s://%s:%d", kongProtocol, kongDataPlaneIPv4, kongDataPlanePort)
+
+	log.Infof("Capifcore URL %s", capifcoreUrl)
+	log.Infof("Kong Control Plane URL %s", kongControlPlaneURL)
+	log.Infof("Kong Data Plane URL %s", kongDataPlaneURL)
+
+	// Very basic checks
+	_, err := url.ParseRequestURI(capifcoreUrl)
+	if err != nil {
+		err = fmt.Errorf("error parsing Capifcore URL: %s", err)
+		return err
+	}
+	_, err = url.ParseRequestURI(kongControlPlaneURL)
+	if err != nil {
+		err = fmt.Errorf("error parsing Kong Control Plane URL: %s", err)
+		return err
+	}
+	_, err = url.ParseRequestURI(kongDataPlaneURL)
+	if err != nil {
+		err = fmt.Errorf("error parsing Kong Data Plane URL: %s", err)
+		return err
+	}
+
+	return nil
+}
+
+func validateEnv(myEnv map[string]string) error {
+	var err error = nil
+
+	kongDomain := myEnv["KONG_DOMAIN"]
+	kongProtocol := myEnv["KONG_PROTOCOL"]
+	kongControlPlaneIPv4 := myEnv["KONG_CONTROL_PLANE_IPV4"]
+	kongDataPlaneIPv4 := myEnv["KONG_DATA_PLANE_IPV4"]
+	capifProtocol := myEnv["CAPIF_PROTOCOL"]
+	capifIPv4 := myEnv["CAPIF_IPV4"]
+
+	if kongDomain == "" || kongDomain == "<string>" {
+		err = fmt.Errorf("error loading KONG_DOMAIN from .env file: %s", kongDomain)
+	} else if kongProtocol == "" || kongProtocol == "<http or https protocol scheme>" {
+		err = fmt.Errorf("error loading KONG_PROTOCOL from .env file: %s", kongProtocol)
+	} else if kongControlPlaneIPv4 == "" || kongControlPlaneIPv4 == "<host string>" {
+		err = fmt.Errorf("error loading KONG_CONTROL_PLANE_IPV4 from .env file: %s", kongControlPlaneIPv4)
+	} else if kongDataPlaneIPv4 == "" || kongDataPlaneIPv4 == "<host string>" {
+		err = fmt.Errorf("error loading KONG_DATA_PLANE_IPV4 from .env file: %s", kongDataPlaneIPv4)
+	} else if capifProtocol == "" || capifProtocol == "<http or https protocol scheme>" {
+		err = fmt.Errorf("error loading CAPIF_PROTOCOL from .env file: %s", capifProtocol)
+	} else if capifIPv4 == "" || capifIPv4 == "<host string>" || capifIPv4 == "<host>" {
+		err = fmt.Errorf("error loading CAPIF_IPV4 from .env file: %s", capifIPv4)
+	}
+	// TEST_SERVICE_IPV4 is used only by the unit tests and are validated in the unit tests.
+
+	return err
+}
+
 func createMapPorts(myEnv map[string]string) (map[string]int, error) {
     myPorts := make(map[string]int)
 	var err error
 
 	myPorts["KONG_DATA_PLANE_PORT"], err = strconv.Atoi(myEnv["KONG_DATA_PLANE_PORT"])
 	if err != nil {
-		log.Fatalf("error loading KONG_DATA_PLANE_PORT from .env file: %s", err)
+		err = fmt.Errorf("error loading KONG_DATA_PLANE_PORT from .env file: %s", err)
 		return nil, err
 	}
 
 	myPorts["KONG_CONTROL_PLANE_PORT"], err = strconv.Atoi(myEnv["KONG_CONTROL_PLANE_PORT"])
 	if err != nil {
-		log.Fatalf("error loading KONG_CONTROL_PLANE_PORT from .env file: %s", err)
+		err = fmt.Errorf("error loading KONG_CONTROL_PLANE_PORT from .env file: %s", err)
 		return nil, err
 	}
 
 	myPorts["CAPIF_PORT"], err = strconv.Atoi(myEnv["CAPIF_PORT"])
 	if err != nil {
-		log.Fatalf("error loading CAPIF_PORT from .env file: %s", err)
+		err = fmt.Errorf("error loading CAPIF_PORT from .env file: %s", err)
 		return nil, err
 	}
 
 	myPorts["SERVICE_MANAGER_PORT"], err = strconv.Atoi(myEnv["SERVICE_MANAGER_PORT"])
 	if err != nil {
-		log.Fatalf("error loading SERVICE_MANAGER_PORT from .env file: %s", err)
+		err = fmt.Errorf("error loading SERVICE_MANAGER_PORT from .env file: %s", err)
 		return nil, err
 	}
 
@@ -142,7 +219,7 @@
 	if myEnv["TEST_SERVICE_PORT"] != "" {
 		myPorts["TEST_SERVICE_PORT"], err = strconv.Atoi(myEnv["TEST_SERVICE_PORT"])
 		if err != nil {
-			log.Fatalf("error loading TEST_SERVICE_PORT from .env file: %s", err)
+			err = fmt.Errorf("error loading TEST_SERVICE_PORT from .env file: %s", err)
 			return nil, err
 		}
 	}
diff --git a/servicemanager/internal/publishservice/publishservice_test.go b/servicemanager/internal/publishservice/publishservice_test.go
index c42cba7..31d5293 100644
--- a/servicemanager/internal/publishservice/publishservice_test.go
+++ b/servicemanager/internal/publishservice/publishservice_test.go
@@ -202,6 +202,24 @@
 	result = testutil.NewRequest().Delete("/published-apis/v1/"+apfId+"/service-apis/"+apiId).Go(t, eServiceManager)
 	assert.Equal(t, http.StatusNoContent, result.Code())
 
+	apiName = "helloworld-v1"
+	apiId = "api_id_" + apiName
+
+	result = testutil.NewRequest().Delete("/published-apis/v1/"+apfId+"/service-apis/"+apiId).Go(t, eServiceManager)
+	assert.Equal(t, http.StatusNoContent, result.Code())
+
+	apiName = "helloworld-v1-id"
+	apiId = "api_id_" + apiName
+
+	result = testutil.NewRequest().Delete("/published-apis/v1/"+apfId+"/service-apis/"+apiId).Go(t, eServiceManager)
+	assert.Equal(t, http.StatusNoContent, result.Code())
+
+	apiName = "helloworld-no-version"
+	apiId = "api_id_" + apiName
+
+	result = testutil.NewRequest().Delete("/published-apis/v1/"+apfId+"/service-apis/"+apiId).Go(t, eServiceManager)
+	assert.Equal(t, http.StatusNoContent, result.Code())
+
 	// Delete the provider
 	domainID := "domain_id_Kong"
 	result = testutil.NewRequest().Delete("/api-provider-management/v1/registrations/"+domainID).Go(t, eServiceManager)
@@ -262,7 +280,11 @@
 	assert.NotZero(t, testServicePort, "TEST_SERVICE_PORT is required in .env file for unit testing")
 
 	apiName := "apiName"
-	newServiceDescription := getServiceAPIDescription(aefId, apiName, description, testServiceIpv4, testServicePort)
+	apiVersion := "v1"
+	resourceName := "helloworld"
+	uri := "/helloworld"
+
+	newServiceDescription := getServiceAPIDescription(aefId, apiName, description, testServiceIpv4, testServicePort, apiVersion, resourceName, uri)
 
 	// Attempt to publish a service for provider
 	result = testutil.NewRequest().Post("/published-apis/v1/"+apfId+"/service-apis").WithJsonBody(newServiceDescription).Go(t, eServiceManager)
@@ -288,7 +310,7 @@
 	assert.NoError(t, err, "error unmarshaling response")
 }
 
-func TestPublishUnpublishService(t *testing.T) {
+func TestPublishUnpublishServiceMissingInterface(t *testing.T) {
 	apfId := "APF_id_rApp_Kong_as_APF"
 	apiName := "apiName"
 
@@ -331,11 +353,37 @@
 	assert.NoError(t, err, "error unmarshaling response")
 
 	assert.Contains(t, *resultError.Cause, "cannot read interfaceDescription")
+}
 
-	newServiceDescription = getServiceAPIDescription(aefId, apiName, description, testServiceIpv4, testServicePort)
+
+func TestPublishUnpublishWithoutVersionId(t *testing.T) {
+	apfId := "APF_id_rApp_Kong_as_APF"
+
+	myEnv, myPorts, err := mockConfigReader.ReadDotEnv()
+	assert.Nil(t, err, "error reading env file")
+
+	testServiceIpv4 := common29122.Ipv4Addr(myEnv["TEST_SERVICE_IPV4"])
+	testServicePort := common29122.Port(myPorts["TEST_SERVICE_PORT"])
+
+	assert.NotEmpty(t, testServiceIpv4, "TEST_SERVICE_IPV4 is required in .env file for unit testing")
+	assert.NotZero(t, testServicePort, "TEST_SERVICE_PORT is required in .env file for unit testing")
+
+	apiVersion := "v1"
+	resourceName := "helloworld"
+	uri := "/helloworld"
+	apiName := "helloworld-v1"
+
+	aefId := "AEF_id_rApp_Kong_as_AEF"
+	namespace := "namespace"
+	repoName := "repoName"
+	chartName := "chartName"
+	releaseName := "releaseName"
+	description := fmt.Sprintf("Description,%s,%s,%s,%s", namespace, repoName, chartName, releaseName)
+
+	newServiceDescription := getServiceAPIDescription(aefId, apiName, description, testServiceIpv4, testServicePort, apiVersion, resourceName, uri)
 
 	// Publish a service for provider
-	result = testutil.NewRequest().Post("/published-apis/v1/"+apfId+"/service-apis").WithJsonBody(newServiceDescription).Go(t, eServiceManager)
+	result := testutil.NewRequest().Post("/published-apis/v1/"+apfId+"/service-apis").WithJsonBody(newServiceDescription).Go(t, eServiceManager)
 	assert.Equal(t, http.StatusCreated, result.Code())
 
 	if result.Code() != http.StatusCreated {
@@ -374,30 +422,184 @@
 	assert.Equal(t, kongDataPlaneIPv4, resultServiceIpv4)
 	assert.Equal(t, kongDataPlanePort, resultServicePort)
 
-	// Publish the same service again should result in Forbidden
-	newServiceDescription.ApiId = &newApiId
-	result = testutil.NewRequest().Post("/published-apis/v1/"+apfId+"/service-apis").WithJsonBody(newServiceDescription).Go(t, eServiceManager)
-	assert.Equal(t, http.StatusForbidden, result.Code())
+	// Check Versions structure
+	version := aefProfile.Versions[0]
+	assert.Equal(t, "v1", version.ApiVersion)
 
-	err = result.UnmarshalBodyToObject(&resultError)
+	resource := (*version.Resources)[0]
+	communicationType := publishapi.CommunicationType("REQUEST_RESPONSE")
+	assert.Equal(t, communicationType, resource.CommType)
+
+	assert.Equal(t, 1, len(*resource.Operations))
+	var operation publishapi.Operation = "GET"
+	assert.Equal(t, operation, (*resource.Operations)[0])
+	assert.Equal(t, "helloworld", resource.ResourceName)
+	assert.Equal(t, "/helloworld-v1/helloworld", resource.Uri)
+}
+
+func TestPublishUnpublishVersionId(t *testing.T) {
+	apfId := "APF_id_rApp_Kong_as_APF"
+
+	myEnv, myPorts, err := mockConfigReader.ReadDotEnv()
+	assert.Nil(t, err, "error reading env file")
+
+	testServiceIpv4 := common29122.Ipv4Addr(myEnv["TEST_SERVICE_IPV4"])
+	testServicePort := common29122.Port(myPorts["TEST_SERVICE_PORT"])
+
+	assert.NotEmpty(t, testServiceIpv4, "TEST_SERVICE_IPV4 is required in .env file for unit testing")
+	assert.NotZero(t, testServicePort, "TEST_SERVICE_PORT is required in .env file for unit testing")
+
+	apiVersion := "v1"
+	resourceName := "helloworld-id"
+	uri := "~/helloworld/(?<helloworld-id>[a-zA-Z0-9]+([-_][a-zA-Z0-9]+)*)"
+	apiName := "helloworld-v1-id"
+
+	aefId := "AEF_id_rApp_Kong_as_AEF"
+	namespace := "namespace"
+	repoName := "repoName"
+	chartName := "chartName"
+	releaseName := "releaseName"
+	description := fmt.Sprintf("Description,%s,%s,%s,%s", namespace, repoName, chartName, releaseName)
+
+	newServiceDescription := getServiceAPIDescription(aefId, apiName, description, testServiceIpv4, testServicePort, apiVersion, resourceName, uri)
+
+	// Publish a service for provider
+	result := testutil.NewRequest().Post("/published-apis/v1/"+apfId+"/service-apis").WithJsonBody(newServiceDescription).Go(t, eServiceManager)
+	assert.Equal(t, http.StatusCreated, result.Code())
+
+	if result.Code() != http.StatusCreated {
+		log.Fatalf("failed to publish the service with HTTP result code %d", result.Code())
+		return
+	}
+
+	var resultService publishapi.ServiceAPIDescription
+	err = result.UnmarshalJsonToObject(&resultService)
 	assert.NoError(t, err, "error unmarshaling response")
-	assert.Contains(t, *resultError.Cause, "already published")
-	assert.Equal(t, http.StatusForbidden, *resultError.Status)
+	newApiId := "api_id_" + apiName
+	assert.Equal(t, newApiId, *resultService.ApiId)
 
-	// Delete the service
-	result = testutil.NewRequest().Delete("/published-apis/v1/"+apfId+"/service-apis/"+newApiId).Go(t, eServiceManager)
-	assert.Equal(t, http.StatusNoContent, result.Code())
+	assert.Equal(t, "http://example.com/published-apis/v1/"+apfId+"/service-apis/"+*resultService.ApiId, result.Recorder.Header().Get(echo.HeaderLocation))
 
-	// Check no services published
-	result = testutil.NewRequest().Get("/published-apis/v1/"+apfId+"/service-apis").Go(t, eServiceManager)
+	// Check that the service is published for the provider
+	result = testutil.NewRequest().Get("/published-apis/v1/"+apfId+"/service-apis/"+newApiId).Go(t, eServiceManager)
 	assert.Equal(t, http.StatusOK, result.Code())
 
-	// Parse JSON from the response body
-	err = result.UnmarshalJsonToObject(&resultServices)
+	err = result.UnmarshalJsonToObject(&resultService)
 	assert.NoError(t, err, "error unmarshaling response")
+	assert.Equal(t, newApiId, *resultService.ApiId)
 
-	// Check if the parsed array is empty
-	assert.Zero(t, len(resultServices))
+	aefProfile := (*resultService.AefProfiles)[0]
+	interfaceDescription := (*aefProfile.InterfaceDescriptions)[0]
+
+	resultServiceIpv4 := *interfaceDescription.Ipv4Addr
+	resultServicePort := *interfaceDescription.Port
+
+	kongDataPlaneIPv4 := common29122.Ipv4Addr(myEnv["KONG_DATA_PLANE_IPV4"])
+	kongDataPlanePort := common29122.Port(myPorts["KONG_DATA_PLANE_PORT"])
+
+	assert.NotEmpty(t, kongDataPlaneIPv4, "KONG_DATA_PLANE_IPV4 is required in .env file for unit testing")
+	assert.NotZero(t, kongDataPlanePort, "KONG_DATA_PLANE_PORT is required in .env file for unit testing")
+
+	assert.Equal(t, kongDataPlaneIPv4, resultServiceIpv4)
+	assert.Equal(t, kongDataPlanePort, resultServicePort)
+
+	// Check Versions structure
+	version := aefProfile.Versions[0]
+	assert.Equal(t, "v1", version.ApiVersion)
+
+	resource := (*version.Resources)[0]
+	communicationType := publishapi.CommunicationType("REQUEST_RESPONSE")
+	assert.Equal(t, communicationType, resource.CommType)
+
+	assert.Equal(t, 1, len(*resource.Operations))
+	var operation publishapi.Operation = "GET"
+	assert.Equal(t, operation, (*resource.Operations)[0])
+
+	assert.Equal(t, "helloworld-id", resource.ResourceName)
+	assert.Equal(t, "~/helloworld-v1-id/helloworld/v1/(?<helloworld-id>[a-zA-Z0-9]+([-_][a-zA-Z0-9]+)*)", resource.Uri)
+}
+
+func TestPublishUnpublishServiceNoVersionWithId(t *testing.T) {
+	apfId := "APF_id_rApp_Kong_as_APF"
+
+	myEnv, myPorts, err := mockConfigReader.ReadDotEnv()
+	assert.Nil(t, err, "error reading env file")
+
+	testServiceIpv4 := common29122.Ipv4Addr(myEnv["TEST_SERVICE_IPV4"])
+	testServicePort := common29122.Port(myPorts["TEST_SERVICE_PORT"])
+
+	assert.NotEmpty(t, testServiceIpv4, "TEST_SERVICE_IPV4 is required in .env file for unit testing")
+	assert.NotZero(t, testServicePort, "TEST_SERVICE_PORT is required in .env file for unit testing")
+
+	apiVersion := ""
+	resourceName := "helloworld-no-version"
+	uri := "~/helloworld/(?<helloworld-id>[a-zA-Z0-9]+([-_][a-zA-Z0-9]+)*)"
+	apiName := "helloworld-no-version"
+
+	aefId := "AEF_id_rApp_Kong_as_AEF"
+	namespace := "namespace"
+	repoName := "repoName"
+	chartName := "chartName"
+	releaseName := "releaseName"
+	description := fmt.Sprintf("Description,%s,%s,%s,%s", namespace, repoName, chartName, releaseName)
+
+	newServiceDescription := getServiceAPIDescription(aefId, apiName, description, testServiceIpv4, testServicePort, apiVersion, resourceName, uri)
+
+	// Publish a service for provider
+	result := testutil.NewRequest().Post("/published-apis/v1/"+apfId+"/service-apis").WithJsonBody(newServiceDescription).Go(t, eServiceManager)
+	assert.Equal(t, http.StatusCreated, result.Code())
+
+	if result.Code() != http.StatusCreated {
+		log.Fatalf("failed to publish the service with HTTP result code %d", result.Code())
+		return
+	}
+
+	var resultService publishapi.ServiceAPIDescription
+	err = result.UnmarshalJsonToObject(&resultService)
+	assert.NoError(t, err, "error unmarshaling response")
+	newApiId := "api_id_" + apiName
+	assert.Equal(t, newApiId, *resultService.ApiId)
+
+	assert.Equal(t, "http://example.com/published-apis/v1/"+apfId+"/service-apis/"+*resultService.ApiId, result.Recorder.Header().Get(echo.HeaderLocation))
+
+	// Check that the service is published for the provider
+	result = testutil.NewRequest().Get("/published-apis/v1/"+apfId+"/service-apis/"+newApiId).Go(t, eServiceManager)
+	assert.Equal(t, http.StatusOK, result.Code())
+
+	err = result.UnmarshalJsonToObject(&resultService)
+	assert.NoError(t, err, "error unmarshaling response")
+	assert.Equal(t, newApiId, *resultService.ApiId)
+
+	aefProfile := (*resultService.AefProfiles)[0]
+	interfaceDescription := (*aefProfile.InterfaceDescriptions)[0]
+
+	resultServiceIpv4 := *interfaceDescription.Ipv4Addr
+	resultServicePort := *interfaceDescription.Port
+
+	kongDataPlaneIPv4 := common29122.Ipv4Addr(myEnv["KONG_DATA_PLANE_IPV4"])
+	kongDataPlanePort := common29122.Port(myPorts["KONG_DATA_PLANE_PORT"])
+
+	assert.NotEmpty(t, kongDataPlaneIPv4, "KONG_DATA_PLANE_IPV4 is required in .env file for unit testing")
+	assert.NotZero(t, kongDataPlanePort, "KONG_DATA_PLANE_PORT is required in .env file for unit testing")
+
+	assert.Equal(t, kongDataPlaneIPv4, resultServiceIpv4)
+	assert.Equal(t, kongDataPlanePort, resultServicePort)
+
+	// Check Versions structure
+	version := aefProfile.Versions[0]
+	assert.Equal(t, "", version.ApiVersion)
+
+	resource := (*version.Resources)[0]
+	communicationType := publishapi.CommunicationType("REQUEST_RESPONSE")
+	assert.Equal(t, communicationType, resource.CommType)
+
+	assert.Equal(t, 1, len(*resource.Operations))
+	var operation publishapi.Operation = "GET"
+	assert.Equal(t, operation, (*resource.Operations)[0])
+
+	assert.Equal(t, "helloworld-no-version", resource.ResourceName)
+	assert.Equal(t, "~/helloworld-no-version/helloworld/(?<helloworld-id>[a-zA-Z0-9]+([-_][a-zA-Z0-9]+)*)", resource.Uri)
+
 	capifCleanUp()
 }
 
@@ -449,7 +651,16 @@
 	return err
 }
 
-func getServiceAPIDescription(aefId, apiName, description string, testServiceIpv4 common29122.Ipv4Addr, testServicePort common29122.Port) publishapi.ServiceAPIDescription {
+func getServiceAPIDescription(
+	aefId string,
+	apiName string,
+	description string,
+	testServiceIpv4 common29122.Ipv4Addr,
+	testServicePort common29122.Port,
+	apiVersion string,
+	resourceName string,
+	uri string) publishapi.ServiceAPIDescription {
+
 	domainName := "Kong"
 	var protocol publishapi.Protocol = "HTTP_1_1"
 
@@ -470,15 +681,15 @@
 				Protocol:   &protocol,
 				Versions: []publishapi.Version{
 					{
-						ApiVersion: "v1",
+						ApiVersion: apiVersion,
 						Resources: &[]publishapi.Resource{
 							{
 								CommType: "REQUEST_RESPONSE",
 								Operations: &[]publishapi.Operation{
 									"GET",
 								},
-								ResourceName: "helloworld",
-								Uri:          "/helloworld",
+								ResourceName: resourceName,
+								Uri:          uri,
 							},
 						},
 					},
diff --git a/servicemanager/internal/publishserviceapi/typeupdate.go b/servicemanager/internal/publishserviceapi/typeupdate.go
index 3b46a0f..5ecc7e5 100644
--- a/servicemanager/internal/publishserviceapi/typeupdate.go
+++ b/servicemanager/internal/publishserviceapi/typeupdate.go
@@ -25,6 +25,7 @@
 	"fmt"
 	"net/http"
 	"net/url"
+	"regexp"
 	"strings"
 
 	resty "github.com/go-resty/resty/v2"
@@ -115,33 +116,63 @@
 	log.Debugf("createKongRoute, routeName %s", routeName)
 	log.Debugf("createKongRoute, aefId %s", aefId)
 
-	uri := buildUri(apiVersion, resource.Uri)
+	uri := insertVersion(apiVersion, resource.Uri)
 	log.Debugf("createKongRoute, uri %s", uri)
 
-	routeUri := buildUri(sd.ApiName, uri)
+	// Create a url.Values map to hold the form data
+	data := url.Values{}
+	serviceUri := uri
+
+	foundRegEx := false
+	if strings.HasPrefix(uri, "~") {
+		log.Debug("createKongRoute, found regex prefix")
+		foundRegEx = true
+		data.Set("strip_path", "false")
+		serviceUri = "/"
+	} else {
+		log.Debug("createKongRoute, no regex prefix found")
+		data.Set("strip_path", "true")
+	}
+
+	log.Debugf("createKongRoute, serviceUri %s", serviceUri)
+	log.Debugf("createKongRoute, strip_path %s", data.Get("strip_path"))
+
+	routeUri := prependUri(sd.ApiName, uri)
 	log.Debugf("createKongRoute, routeUri %s", routeUri)
 	resource.Uri = routeUri
 
-	statusCode, err := sd.createKongService(kongControlPlaneURL, serviceName, uri, tags)
-	if (err != nil) || (statusCode != http.StatusCreated) {
+	statusCode, err := sd.createKongService(kongControlPlaneURL, serviceName, serviceUri, tags)
+	if (err != nil) || ((statusCode != http.StatusCreated) && (statusCode != http.StatusForbidden)) {
+		// We carry on if we tried to create a duplicate service. We depend on Kong route matching.
 		return statusCode, err
 	}
 
-	kongRoutesURL := kongControlPlaneURL + "/services/" + serviceName + "/routes"
+	data.Set("name", routeName)
 
-	// Define the route information for Kong
-	kongRouteInfo := map[string]interface{}{
-		"name":       routeName,
-		"paths":      []string{routeUri},
-		"methods":    resource.Operations,
-		"tags":       tags,
-		"strip_path": true,
-	}
+	routeUriPaths := []string{routeUri}
+	for _, path := range routeUriPaths {
+		log.Debugf("createKongRoute, path %s", path)
+		data.Add("paths", path)
+    }
+
+	for _, tag := range tags {
+		log.Debugf("createKongRoute, tag %s", tag)
+		data.Add("tags", tag)
+    }
+
+	for _, op := range *resource.Operations {
+		log.Debugf("createKongRoute, op %s", string(op))
+		data.Add("methods", string(op))
+    }
+
+	// Encode the data to application/x-www-form-urlencoded format
+	encodedData := data.Encode()
 
 	// Make the POST request to create the Kong service
+	kongRoutesURL := kongControlPlaneURL + "/services/" + serviceName + "/routes"
 	resp, err := client.R().
-		SetHeader("Content-Type", "application/json").
-		SetBody(kongRouteInfo).
+		SetHeader("Content-Type", "application/x-www-form-urlencoded").
+		SetBody(strings.NewReader(encodedData)).
 		Post(kongRoutesURL)
 
 	// Check for errors in the request
@@ -153,6 +184,12 @@
 	// Check the response status code
 	if resp.StatusCode() == http.StatusCreated {
 		log.Infof("kong route %s created successfully", routeName)
+		if (foundRegEx) {
+			statusCode, err := sd.createRequestTransformer(kongControlPlaneURL, client, routeName, uri)
+			if (err != nil) || ((statusCode != http.StatusCreated) && (statusCode != http.StatusForbidden)) {
+				return statusCode, err
+			}
+		}
 	} else {
 		log.Debugf("kongRoutesURL %s", kongRoutesURL)
 		err = fmt.Errorf("error creating Kong route. Status code: %d", resp.StatusCode())
@@ -164,15 +201,146 @@
 	return resp.StatusCode(), nil
 }
 
-func buildUri(prependUri string, uri string) string {
+func (sd *ServiceAPIDescription) createRequestTransformer(
+	kongControlPlaneURL string,
+	client *resty.Client,
+	routeName string,
+	routePattern string) (int, error) {
+
+	log.Trace("entering createRequestTransformer")
+
+	// Make the POST request to create the Kong Request Transformer
+	kongRequestTransformerURL := kongControlPlaneURL + "/routes/" + routeName + "/plugins"
+
+	transformPattern, _ := deriveTransformPattern(routePattern)
+
+	// Create the form data
+	formData := url.Values{
+		"name":                  {"request-transformer"},
+		"config.replace.uri":    {transformPattern},
+	}
+	encodedData := formData.Encode()
+
+	// Create a new HTTP POST request
+	resp, err := client.R().
+		SetHeader("Content-Type", "application/x-www-form-urlencoded").
+		SetBody(strings.NewReader(encodedData)).
+		Post(kongRequestTransformerURL)
+
+	// Check for errors in the request
+	if err != nil {
+		log.Debugf("createRequestTransformer POST Error: %v", err)
+		return resp.StatusCode(), err
+	}
+
+	// Check the response status code
+	if resp.StatusCode() == http.StatusCreated {
+		log.Infof("kong request transformer created successfully for route %s", routeName)
+	} else {
+		log.Debugf("kongRequestTransformerURL %s", kongRequestTransformerURL)
+		err = fmt.Errorf("error creating Kong request transformer. Status code: %d", resp.StatusCode())
+		log.Error(err.Error())
+		log.Errorf("response body: %s", resp.Body())
+		return resp.StatusCode(), err
+	}
+
+	return resp.StatusCode(), nil
+}
+
+// Function to derive the transform pattern from the route pattern
+func deriveTransformPattern(routePattern string) (string, error) {
+	log.Trace("entering deriveTransformPattern")
+
+	log.Debugf("deriveTransformPattern routePattern %s", routePattern)
+
+	routePattern = strings.TrimPrefix(routePattern, "~")
+	log.Debugf("deriveTransformPattern, TrimPrefix trimmed routePattern %s", routePattern)
+
+	// Append a slash to handle an edge case for matching a trailing capture group.
+	appendedSlash := false
+	if routePattern[len(routePattern)-1] != '/' {
+		routePattern = routePattern + "/"
+		appendedSlash = true
+		log.Debugf("deriveTransformPattern, append / routePattern %s", routePattern)
+	}
+
+	// Regular expression to match named capture groups
+	re := regexp.MustCompile(`/\(\?<([^>]+)>([^\/]+)/`)
+	// Find all matches in the route pattern
+	matches := re.FindAllStringSubmatch(routePattern, -1)
+
+	transformPattern := routePattern
+	for _, match := range matches {
+		// match[0] is the full match, match[1] is the capture group name, match[2] is the pattern
+		placeholder := fmt.Sprintf("/$(uri_captures[\"%s\"])/", match[1])
+		// Replace the capture group with the corresponding placeholder
+		transformPattern = strings.Replace(transformPattern, match[0], placeholder, 1)
+	}
+	log.Debugf("deriveTransformPattern transformPattern %s", transformPattern)
+
+	if appendedSlash {
+		transformPattern = strings.TrimSuffix(transformPattern, "/")
+		log.Debugf("deriveTransformPattern, remove / transformPattern %s", transformPattern)
+	}
+
+	return transformPattern, nil
+}
+
+func insertVersion(version string, route string) string {
+	versionedRoute := route
+
+	if version != "" {
+		sep := "/"
+		n := 3
+
+		foundRegEx := false
+		if strings.HasPrefix(route, "~") {
+			log.Debug("insertVersion, found regex prefix")
+			foundRegEx = true
+			route = strings.TrimPrefix(route, "~")
+		}
+
+		log.Debugf("insertVersion route %s", route)
+		split := strings.SplitAfterN(route, sep, n)
+		log.Debugf("insertVersion split %q", split)
+
+		versionedRoute = split[0]
+		if len(split) == 2 {
+			versionedRoute = split[0] + split[1]
+		} else if len(split) > 2 {
+			versionedRoute = split[0] + split[1] + version + sep + split[2]
+		}
+
+		if foundRegEx {
+			versionedRoute = "~" + versionedRoute
+		}
+	}
+	log.Debugf("insertVersion versionedRoute %s", versionedRoute)
+
+	return versionedRoute
+}
+
+func prependUri(prependUri string, uri string) string {
 	if prependUri != "" {
+		trimmedUri := uri
+		foundRegEx := false
+		if strings.HasPrefix(uri, "~") {
+			log.Debug("prependUri, found regex prefix")
+			foundRegEx = true
+			trimmedUri = strings.TrimPrefix(uri, "~")
+			log.Debugf("prependUri, TrimPrefix trimmedUri %s", trimmedUri)
+		}
+
 		if prependUri[0] != '/' {
 			prependUri = "/" + prependUri
 		}
-		if prependUri[len(prependUri)-1] != '/' && uri[0] != '/' {
+		if prependUri[len(prependUri)-1] != '/' && trimmedUri[0] != '/' {
 			prependUri = prependUri + "/"
 		}
-		uri = prependUri + uri
+		uri = prependUri + trimmedUri
+		if foundRegEx {
+			uri = "~" + uri
+		}
 	}
 	return uri
 }
diff --git a/servicemanager/main.go b/servicemanager/main.go
index b90782c..d0fbe4f 100644
--- a/servicemanager/main.go
+++ b/servicemanager/main.go
@@ -48,8 +48,9 @@
 func main() {
 	realConfigReader := &envreader.RealConfigReader{}
 	myEnv, myPorts, err := realConfigReader.ReadDotEnv()
+
 	if err != nil {
-		log.Fatal("error loading environment file")
+		log.Fatalf("error loading environment file, %v", err)
 		return
 	}
 
@@ -87,7 +88,7 @@
 	// Register ProviderManagement
 	providerManagerSwagger, err := providermanagementapi.GetSwagger()
 	if err != nil {
-		log.Fatalf("error loading ProviderManagement swagger spec\n: %s", err)
+		log.Fatalf("error loading ProviderManagement swagger spec\n: %v", err)
 		return err
 	}
 	providerManagerSwagger.Servers = nil
@@ -99,7 +100,7 @@
 	// Register PublishService
 	publishServiceSwagger, err := publishserviceapi.GetSwagger()
 	if err != nil {
-		log.Fatalf("error loading PublishService swagger spec\n: %s", err)
+		log.Fatalf("error loading PublishService swagger spec\n: %v", err)
 		return err
 	}
 	publishServiceSwagger.Servers = nil
@@ -116,7 +117,7 @@
 	// Register InvokerManagement
 	invokerManagerSwagger, err := invokermanagementapi.GetSwagger()
 	if err != nil {
-		log.Fatalf("error loading InvokerManagement swagger spec\n: %s", err)
+		log.Fatalf("error loading InvokerManagement swagger spec\n: %v", err)
 		return err
 	}
 	invokerManagerSwagger.Servers = nil
@@ -128,7 +129,7 @@
 	// Register DiscoverService
 	discoverServiceSwagger, err := discoverserviceapi.GetSwagger()
 	if err != nil {
-		log.Fatalf("error loading DiscoverService swagger spec\n: %s", err)
+		log.Fatalf("error loading DiscoverService swagger spec\n: %v", err)
 		return err
 	}
 
diff --git a/servicemanager/mockkong/kong_mock.go b/servicemanager/mockkong/kong_mock.go
index 14acd57..c0a6dba 100644
--- a/servicemanager/mockkong/kong_mock.go
+++ b/servicemanager/mockkong/kong_mock.go
@@ -85,6 +85,78 @@
 		return c.String(http.StatusCreated, string(body))
 	})
 
+	e.POST("/services/api_id_apiName_helloworld-id/routes", func(c echo.Context) error {
+		body, err := io.ReadAll(c.Request().Body)
+		if err != nil {
+			return c.String(http.StatusInternalServerError, "Error reading request body")
+		}
+		return c.String(http.StatusCreated, string(body))
+	})
+
+	e.POST("/services/api_id_apiName_helloworld-no-version/routes", func(c echo.Context) error {
+		body, err := io.ReadAll(c.Request().Body)
+		if err != nil {
+			return c.String(http.StatusInternalServerError, "Error reading request body")
+		}
+		return c.String(http.StatusCreated, string(body))
+	})
+
+	e.POST("/services/api_id_helloworld-v1-id_helloworld-id/routes", func(c echo.Context) error {
+		body, err := io.ReadAll(c.Request().Body)
+		if err != nil {
+			return c.String(http.StatusInternalServerError, "Error reading request body")
+		}
+		return c.String(http.StatusCreated, string(body))
+	})
+
+	e.POST("/services/api_id_helloworld-v1_helloworld/routes", func(c echo.Context) error {
+		body, err := io.ReadAll(c.Request().Body)
+		if err != nil {
+			return c.String(http.StatusInternalServerError, "Error reading request body")
+		}
+		return c.String(http.StatusCreated, string(body))
+	})
+
+	e.POST("/services/api_id_helloworld-no-version_helloworld-no-version/routes", func(c echo.Context) error {
+		body, err := io.ReadAll(c.Request().Body)
+		if err != nil {
+			return c.String(http.StatusInternalServerError, "Error reading request body")
+		}
+		return c.String(http.StatusCreated, string(body))
+	})
+
+	e.POST("/routes/api_id_apiName_helloworld-id/plugins", func(c echo.Context) error {
+		body, err := io.ReadAll(c.Request().Body)
+		if err != nil {
+			return c.String(http.StatusInternalServerError, "Error reading request body")
+		}
+		return c.String(http.StatusCreated, string(body))
+	})
+
+	e.POST("/routes/api_id_apiName_helloworld-no-version/plugins", func(c echo.Context) error {
+		body, err := io.ReadAll(c.Request().Body)
+		if err != nil {
+			return c.String(http.StatusInternalServerError, "Error reading request body")
+		}
+		return c.String(http.StatusCreated, string(body))
+	})
+
+	e.POST("/routes/api_id_helloworld-v1-id_helloworld-id/plugins", func(c echo.Context) error {
+		body, err := io.ReadAll(c.Request().Body)
+		if err != nil {
+			return c.String(http.StatusInternalServerError, "Error reading request body")
+		}
+		return c.String(http.StatusCreated, string(body))
+	})
+
+	e.POST("/routes/api_id_helloworld-no-version_helloworld-no-version/plugins", func(c echo.Context) error {
+		body, err := io.ReadAll(c.Request().Body)
+		if err != nil {
+			return c.String(http.StatusInternalServerError, "Error reading request body")
+		}
+		return c.String(http.StatusCreated, string(body))
+	})
+
 	e.GET("/services", func(c echo.Context) error {
 		return c.String(http.StatusOK, "{}")
 	})
@@ -132,4 +204,45 @@
 	e.DELETE("/services/api_id_apiName2_app", func(c echo.Context) error {
 		return c.NoContent(http.StatusNoContent)
 	})
+
+	e.DELETE("/routes/api_id_apiName_helloworld-id", func(c echo.Context) error {
+		return c.NoContent(http.StatusNoContent)
+	})
+
+	e.DELETE("/routes/api_id_apiName_helloworld-no-version", func(c echo.Context) error {
+		return c.NoContent(http.StatusNoContent)
+	})
+
+	e.DELETE("/routes/api_id_helloworld-v1_helloworld", func(c echo.Context) error {
+		return c.NoContent(http.StatusNoContent)
+	})
+
+	e.DELETE("/services/api_id_apiName_helloworld-id", func(c echo.Context) error {
+		return c.NoContent(http.StatusNoContent)
+	})
+
+	e.DELETE("/services/api_id_apiName_helloworld-no-version", func(c echo.Context) error {
+		return c.NoContent(http.StatusNoContent)
+	})
+
+	e.DELETE("/services/api_id_helloworld-v1_helloworld", func(c echo.Context) error {
+		return c.NoContent(http.StatusNoContent)
+	})
+
+	e.DELETE("routes/api_id_helloworld-no-version_helloworld-no-version", func(c echo.Context) error {
+		return c.NoContent(http.StatusNoContent)
+	})
+
+	e.DELETE("services/api_id_helloworld-no-version_helloworld-no-version", func(c echo.Context) error {
+		return c.NoContent(http.StatusNoContent)
+	})
+
+	e.DELETE("/routes/api_id_helloworld-v1-id_helloworld-id", func(c echo.Context) error {
+		return c.NoContent(http.StatusNoContent)
+	})
+
+	e.DELETE("/services/api_id_helloworld-v1-id_helloworld-id", func(c echo.Context) error {
+		return c.NoContent(http.StatusNoContent)
+	})
+
 }
\ No newline at end of file