- update yauaa to 5.21 ( CVE-2020-13956)
- update junit (CVE-2020-15250)
- add JSON url mapper (capability already included) and test
- TurbineURLMapperService: Adding behaviour for ignore parameter as capturing group: will ignore parameter value in mapToUrl. Example in turbine-url-mapping.json
- fix defaultOnBorrow warning
- add missing yaml test and TR.properties
- add xdoc draft for url mapper

git-svn-id: https://svn.apache.org/repos/asf/turbine/core/trunk@1886168 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/conf/test/TorqueTest.properties b/conf/test/TorqueTest.properties
index 7cfcd5f..d04f0f1 100644
--- a/conf/test/TorqueTest.properties
+++ b/conf/test/TorqueTest.properties
@@ -18,7 +18,7 @@
 torque.applicationRoot = .
 pipeline.default.descriptor = conf/turbine-classic-pipeline.xml
 # torque.defaults.pool.defaultMaxActive=30
-torque.defaults.pool.testOnBorrow=true
+torque.defaults.pool.defaultTestOnBorrow=true
 torque.defaults.pool.validationQuery=SELECT 1
 
 torque.idbroker.cleverquantity=true
@@ -32,7 +32,7 @@
 # dbcp2 
 torque.dsfactory.default.factory= org.apache.torque.dsfactory.SharedPool2DataSourceFactory
 
-torque.dsfactory.default.pool.testOnBorrow=true
+torque.dsfactory.default.pool.defaultTestOnBorrow=true
 torque.dsfactory.default.pool.validationQuery=SELECT 1 from INFORMATION_SCHEMA.SYSTEM_USERS
 torque.dsfactory.default.connection.driver = org.hsqldb.jdbcDriver
 torque.dsfactory.default.connection.url = jdbc:hsqldb:.
diff --git a/conf/test/TurbineURLMapperServiceTest.properties b/conf/test/TurbineURLMapperServiceTest.properties
index 53a36d3..0a7c791 100644
--- a/conf/test/TurbineURLMapperServiceTest.properties
+++ b/conf/test/TurbineURLMapperServiceTest.properties
@@ -127,7 +127,24 @@
 
 # -------------------------------------------------------------------
 #
+#  P U L L  S E R V I C E
+#
+# -------------------------------------------------------------------
+
+# services.PullService.classname=org.apache.turbine.services.pull.TurbinePullService
+
+# These tools will be made available to all your
+# templates. You list the tools in the following way:
+#
+# tool.<scope>.<id> = <classname>
+
+# tool.request.mlink=org.apache.turbine.services.urlmapper.MappedTemplateLink
+
+
+# -------------------------------------------------------------------
+#
 #  U R L  M A P P E R  S E R V I C E
 #
 # -------------------------------------------------------------------
+
 services.URLMapperService.configFile = /conf/turbine-url-mapping.xml
diff --git a/conf/test/TurbineURLMapperYAMLServiceTest.properties b/conf/test/TurbineURLMapperYAMLServiceTest.properties
new file mode 100644
index 0000000..8865dae
--- /dev/null
+++ b/conf/test/TurbineURLMapperYAMLServiceTest.properties
@@ -0,0 +1,22 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you 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.
+
+# override
+services.URLMapperService.configFile = /conf/turbine-url-mapping.yml
+
+include = TurbineURLMapperServiceTest.properties
+
diff --git a/conf/turbine-url-mapping.json b/conf/turbine-url-mapping.json
new file mode 100644
index 0000000..731a75b
--- /dev/null
+++ b/conf/turbine-url-mapping.json
@@ -0,0 +1,45 @@
+{
+	"name": "default",
+	"maps": [
+		{
+			"pattern": "/(?<webAppRoot>[\\w]+)/(?<contextPath>\\w+)/register",
+			"implicit-parameters": {
+				"page": "Register",
+				"role": "anon"
+			}
+		},
+		{
+            "pattern": "/(?<webAppRoot>[\\w]+)/(?<contextPath>\\w+)/(?<language>\\w+)/register",
+            "implicit-parameters": {
+                "page": "Register",
+                "role": "anon"
+            },
+            "override-parameters": {
+                "role": "anon"
+            }
+        },
+        {
+            "pattern": "/(?<webAppRoot>[\\w]+)/(?<contextPath>\\w+)/contact",
+            "implicit-parameters": {
+                "page": "Contact",
+                "role": "anon"
+            },
+            "override-parameters": {
+                "role": "anon"
+            }
+        },
+		{
+            "pattern": "/(?<webAppRoot>[\\w]+)/(?<contextPath>\\w+)/(?<language>\\w+)/contact",
+            "implicit-parameters": {
+                "page": "Contact",
+                "role": "anon"
+            },
+            "override-parameters": {
+                "role": "anon"
+            },
+            "ignore-parameters": {
+                "language": null
+            }
+        }
+	]
+}
\ No newline at end of file
diff --git a/conf/turbine-url-mapping.yml b/conf/turbine-url-mapping.yml
index 6a47d64..d3f1781 100644
--- a/conf/turbine-url-mapping.yml
+++ b/conf/turbine-url-mapping.yml
@@ -1,4 +1,3 @@
-
 name: default
 maps:
     - pattern: /(?<contextPath>\w+)/book/(?<bookId>\d+)
@@ -10,22 +9,22 @@
         template: Book.vm
       ignore-parameters:
         view: null
-    - pattern: /(?<contextPath>\w+)/register
+    - pattern: /(?<webAppRoot>[\.\-\w]+)(?<contextPath>\w+)/register
       implicit-parameters:
         media-type: html
         role: anon
         template: Registerone.vm
         js_pane: random-id-123-abc
-    - pattern: /(?<contextPath>\w+)/contact
+    - pattern: /(?<webAppRoot>[\.\-\w]+)(?<contextPath>\w+)/contact
       implicit-parameters:
         media-type: html
         page: Contact
         js_pane: another-random-id-876-dfg       
         role: anon
       override-parameters:
-        role: anon
+        role: anon  
     - pattern: /(?<contextPath>\w+)/(?<id>\d+)/(?<role>\w+)/(?<language>\w+)
       implicit-parameters:
         media-type: html
-        template: default.vm    
+        template: default.vm 
                 
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index 6aa42d7..08796ed 100644
--- a/pom.xml
+++ b/pom.xml
@@ -22,7 +22,7 @@
   <parent>

     <groupId>org.apache.turbine</groupId>

     <artifactId>turbine-parent</artifactId>

-    <version>7</version>

+    <version>8-SNAPSHOT</version>

   </parent>

   <artifactId>turbine</artifactId>

   <name>Apache Turbine</name>

@@ -532,6 +532,7 @@
     <plugins>

 

      <plugin>

+       <!-- hint: mvn verify  -->

         <groupId>org.owasp</groupId>

         <artifactId>dependency-check-maven</artifactId>

         <configuration>

@@ -900,7 +901,7 @@
     <dependency>

       <groupId>nl.basjes.parse.useragent</groupId>

       <artifactId>yauaa</artifactId>

-      <version>5.19</version>

+      <version>5.21</version>

     </dependency>

     <dependency>

       <groupId>org.apache.fulcrum</groupId>

@@ -970,11 +971,6 @@
       </exclusions>

     </dependency>

     <!-- use default dbcp2 from torque-runtime -->

-    <!--dependency> 

-        <groupId>commons-dbcp</groupId>

-        <artifactId>commons-dbcp</artifactId>

-        <version>1.4</version>

-    </dependency-->

     <dependency>

          <groupId>commons-configuration</groupId>

          <artifactId>commons-configuration</artifactId>

@@ -1108,10 +1104,16 @@
       <scope>test</scope>

       <optional>true</optional>

     </dependency>

-    <!-- do not add junit 4 support e.g. with org.junit.platform runner or launcher,

+     <dependency>

+        <groupId>junit</groupId>

+        <artifactId>junit</artifactId>

+        <version>4.13.1</version>

+        <scope>test</scope>

+      </dependency>

+      <!-- do not add junit 4 support e.g. with org.junit.platform runner or launcher,

           as it is not compatible with jupiter tags, will throw 

          [WARNING] Couldn't load group class 'docker' in Surefire|Failsafe plugin

-    -->

+       -->

   </dependencies>

 

   <profiles>

@@ -1252,6 +1254,7 @@
       <activation>

         <activeByDefault>false</activeByDefault>

       </activation>

+      <!-- mvn test -Dtest=TurbineURLMapperYAMLServiceTest -Pyaml -->

       <build>

       <plugins>

             <plugin>

diff --git a/src/java/org/apache/turbine/services/urlmapper/TurbineURLMapperService.java b/src/java/org/apache/turbine/services/urlmapper/TurbineURLMapperService.java
index dc79c2f..941b29b 100644
--- a/src/java/org/apache/turbine/services/urlmapper/TurbineURLMapperService.java
+++ b/src/java/org/apache/turbine/services/urlmapper/TurbineURLMapperService.java
@@ -182,18 +182,21 @@
                         matcher.appendReplacement(sb, uri.getScriptName());
                     } else
                     {
+                        boolean ignore = urlMap.getIgnoreParameters().keySet().stream()
+                                .anyMatch( x-> x.equals( key ) );
                         matcher.appendReplacement(sb,
-                                Matcher.quoteReplacement(
-                                        Objects.toString(uriParameterMap.get(key))));
+                                 Matcher.quoteReplacement(
+                                        (!ignore)? Objects.toString(uriParameterMap.get(key)):""));
                         // Remove handled parameters (all of them!)
                         uri.removePathInfo(key);
                         uri.removeQueryData(key);
                     }
                 }
-
+                
                 matcher.appendTail(sb);
+                
                 // Clean up
-                uri.setScriptName(sb.toString().replace("//", "/"));
+                uri.setScriptName(sb.toString().replaceAll("/+", "/"));
                 break;
             }
         }
@@ -276,6 +279,10 @@
                 // which is not what we need here -> java object deserialization.
                 ObjectMapper mapper = new ObjectMapper(new YAMLFactory());
                 container = mapper.readValue(reader, URLMappingContainer.class);
+            } else if (configFile.endsWith(".json"))
+            {
+                ObjectMapper mapper = new ObjectMapper();
+                container = mapper.readValue(reader, URLMappingContainer.class);
             }
         }
         catch (IOException | JAXBException e)
diff --git a/src/test/org/apache/turbine/services/urlmapper/TurbineURLMapperJSONServiceTest.java b/src/test/org/apache/turbine/services/urlmapper/TurbineURLMapperJSONServiceTest.java
new file mode 100644
index 0000000..e5be01f
--- /dev/null
+++ b/src/test/org/apache/turbine/services/urlmapper/TurbineURLMapperJSONServiceTest.java
@@ -0,0 +1,193 @@
+package org.apache.turbine.services.urlmapper;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.mock;
+
+import javax.servlet.ServletConfig;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.fulcrum.parser.ParameterParser;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.apache.turbine.Turbine;
+import org.apache.turbine.pipeline.PipelineData;
+import org.apache.turbine.services.TurbineServices;
+import org.apache.turbine.test.BaseTestCase;
+import org.apache.turbine.util.RunData;
+import org.apache.turbine.util.TurbineConfig;
+import org.apache.turbine.util.uri.TemplateURI;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+public class TurbineURLMapperJSONServiceTest extends BaseTestCase
+{
+
+    private static TurbineConfig tc = null;
+
+    private static URLMapperService urlMapper = null;
+
+    private RunData data;
+
+    Logger log = LogManager.getLogger();
+
+    @BeforeAll
+    public static void setUp() throws Exception
+    {
+        tc = new TurbineConfig( ".", "/conf/test/TurbineURLMapperJSONServiceTest.properties" );
+        tc.initialize();
+
+        urlMapper = (URLMapperService) TurbineServices.getInstance().getService( URLMapperService.SERVICE_NAME );
+    }
+
+    @AfterAll
+    public static void tearDown() throws Exception
+    {
+        if (tc != null)
+        {
+            tc.dispose();
+        }
+    }
+
+    @BeforeEach
+    public void init() throws Exception
+    {
+
+        ServletConfig config = tc.getTurbine().getServletConfig();
+        // mock(ServletConfig.class);
+        HttpServletRequest request = getMockRequest();
+        HttpServletResponse response = mock( HttpServletResponse.class );
+
+        data = getRunData( request, response, config );
+
+        Mockito.when( response.encodeURL( Mockito.anyString() ) )
+                .thenAnswer( invocation -> invocation.getArgument( 0 ) );
+    }
+
+    @Test
+    public void testIgnoreParameterForShortURL() throws Exception
+    {
+
+        PipelineData pipelineData = data;
+
+        assertNotNull( urlMapper );
+
+        ParameterParser pp = pipelineData.get( Turbine.class, ParameterParser.class );
+        assertNotNull( pp );
+        assertTrue( pp.keySet().isEmpty() );
+        pp.clear();
+
+        urlMapper.mapFromURL( "/app/context/contact", pp );
+
+        log.info( "parameters: {}", pp );
+        assertEquals( 2, pp.keySet().size() );
+        assertEquals( "anon", pp.getString( "role" ) );
+        assertEquals( "Contact", pp.getString( "page" ) );
+
+        TemplateURI uri2 = new TemplateURI( pipelineData.getRunData() );
+        uri2.clearResponse();
+        uri2.addPathInfo( pp );
+
+        // this is an artifical url
+        assertEquals( "scheme://bob/wow/damn2/page/Contact/role/anon", uri2.getAbsoluteLink() );
+
+        uri2.addPathInfo( "language", "en" );
+        assertEquals( "scheme://bob/wow/damn2/page/Contact/role/anon/language/en", uri2.getAbsoluteLink() );
+
+        urlMapper.mapToURL( uri2 );
+        String expectedMappedURL = "/wow/damn2/contact";
+        assertEquals( expectedMappedURL, uri2.getRelativeLink() );
+
+        pp.clear();
+        urlMapper.mapFromURL( uri2.getRelativeLink(), pp );
+
+        log.info( "parameters: {}", pp );
+        assertEquals( 2, pp.keySet().size() );
+        assertEquals( "anon", pp.getString( "role" ) );
+        assertEquals( "Contact", pp.getString( "page" ) );
+
+        uri2 = new TemplateURI( pipelineData.getRunData() );
+        uri2.clearResponse();
+        uri2.addPathInfo( pp );
+
+        urlMapper.mapToURL( uri2 );
+        assertEquals( expectedMappedURL, uri2.getRelativeLink() );
+
+    }
+
+    @Test
+    public void testNonOptionalParameterForShortURL() throws Exception
+    {
+
+        PipelineData pipelineData = data;
+
+        assertNotNull( urlMapper );
+
+        ParameterParser pp = pipelineData.get( Turbine.class, ParameterParser.class );
+        assertNotNull( pp );
+        assertTrue( pp.keySet().isEmpty() );
+        pp.clear();
+
+        urlMapper.mapFromURL( "/wow/damn2/register", pp );
+
+        log.info( "parameters: {}", pp );
+        assertEquals( 2, pp.keySet().size() );
+        assertEquals( "anon", pp.getString( "role" ) );
+        assertEquals( "Register", pp.getString( "page" ) );
+
+        TemplateURI uri2 = new TemplateURI( pipelineData.getRunData() );
+        uri2.clearResponse();
+        uri2.addPathInfo( pp );
+
+        // this is an artifical url
+        assertEquals( "scheme://bob/wow/damn2/page/Register/role/anon", uri2.getAbsoluteLink() );
+
+        uri2.addPathInfo( "language", "en" );
+        assertEquals( "scheme://bob/wow/damn2/page/Register/role/anon/language/en", uri2.getAbsoluteLink() );
+
+        urlMapper.mapToURL( uri2 );
+        String expectedMappedURL = "/wow/damn2/en/register";
+        assertEquals( expectedMappedURL, uri2.getRelativeLink() );
+
+        pp.clear();
+        urlMapper.mapFromURL( uri2.getRelativeLink(), pp );
+
+        log.info( "parameters: {}", pp );
+        assertEquals( 3, pp.keySet().size() );
+        assertEquals( "anon", pp.getString( "role" ) );
+        assertEquals( "Register", pp.getString( "page" ) );
+        assertEquals( "en", pp.getString( "language" ) );
+
+        uri2 = new TemplateURI( pipelineData.getRunData() );
+        uri2.clearResponse();
+        uri2.addPathInfo( pp );
+
+        urlMapper.mapToURL( uri2 );
+        assertEquals( expectedMappedURL, uri2.getRelativeLink() );
+
+    }
+
+}
diff --git a/src/test/org/apache/turbine/services/urlmapper/TurbineURLMapperYAMLServiceTest.java b/src/test/org/apache/turbine/services/urlmapper/TurbineURLMapperYAMLServiceTest.java
new file mode 100644
index 0000000..79c553a
--- /dev/null
+++ b/src/test/org/apache/turbine/services/urlmapper/TurbineURLMapperYAMLServiceTest.java
@@ -0,0 +1,213 @@
+package org.apache.turbine.services.urlmapper;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.mock;
+
+import javax.servlet.ServletConfig;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.fulcrum.parser.ParameterParser;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.apache.turbine.Turbine;
+import org.apache.turbine.pipeline.PipelineData;
+import org.apache.turbine.services.TurbineServices;
+import org.apache.turbine.test.BaseTestCase;
+import org.apache.turbine.util.RunData;
+import org.apache.turbine.util.TurbineConfig;
+import org.apache.turbine.util.uri.TemplateURI;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+@Tag("yaml")
+public class TurbineURLMapperYAMLServiceTest extends BaseTestCase
+{
+
+    private static TurbineConfig tc = null;
+
+    private static URLMapperService urlMapper = null;
+
+    private RunData data;
+
+    Logger log = LogManager.getLogger();
+
+    @BeforeAll
+    public static void setUp() throws Exception
+    {
+        tc = new TurbineConfig( ".", "/conf/test/TurbineURLMapperYAMLServiceTest.properties" );
+        tc.initialize();
+
+        urlMapper = (URLMapperService) TurbineServices.getInstance().getService( URLMapperService.SERVICE_NAME );
+    }
+
+    @AfterAll
+    public static void tearDown() throws Exception
+    {
+        if (tc != null)
+        {
+            tc.dispose();
+        }
+    }
+
+    @BeforeEach
+    public void init() throws Exception
+    {
+
+        ServletConfig config = tc.getTurbine().getServletConfig();
+        // mock(ServletConfig.class);
+        HttpServletRequest request = getMockRequest();
+        HttpServletResponse response = mock( HttpServletResponse.class );
+
+        data = getRunData( request, response, config );
+
+        Mockito.when( response.encodeURL( Mockito.anyString() ) )
+                .thenAnswer( invocation -> invocation.getArgument( 0 ) );
+    }
+
+    @Test
+    public void testMapToAnotherURL() throws Exception
+    {
+
+        PipelineData pipelineData = data;
+
+        assertNotNull( urlMapper );
+
+        TemplateURI uri = new TemplateURI( pipelineData.getRunData() );
+        uri.addPathInfo( "id", 1234 );
+        uri.addPathInfo( "role", "guest" );
+        uri.addPathInfo( "language", "de" );
+
+        String unMappedURL = uri.getAbsoluteLink(); // scheme://bob/wow/damn2/id/1234/role/guest
+        log.info( unMappedURL );
+
+        String expectedRawURL = "scheme://bob/wow/damn2/id/1234/role/guest/language/de";
+        urlMapper.mapToURL( uri );
+        urlMapper.mapToURL( uri ); // should be idempotent
+        // raw url
+        assertEquals( expectedRawURL, unMappedURL );
+
+        String mappedLink = uri.getRelativeLink(); // wow/damn2/id/1234/role/guest
+        log.info( mappedLink );
+        String expectedMappedURL = "/wow/1234/guest/de";
+        assertEquals( expectedMappedURL, mappedLink );
+
+        ParameterParser pp = pipelineData.get( Turbine.class, ParameterParser.class );
+        assertNotNull( pp );
+        assertTrue( pp.keySet().isEmpty() );
+        urlMapper.mapFromURL( mappedLink, pp );
+
+        assertEquals( 5, pp.keySet().size() );
+        assertEquals( 1234, pp.getInt( "id" ) );
+        assertEquals( "guest", pp.getString( "role" ) );
+        assertEquals( "de", pp.getString( "language" ) );
+        assertEquals( "html", pp.getString( "media-type" ) );
+
+        TemplateURI uri2 = new TemplateURI( pipelineData.getRunData() );
+        uri2.clearResponse();
+        uri2.setTemplate( "default.vm" );
+        uri2.addPathInfo( pp );
+        // this is an artifical url
+        assertEquals( "scheme://bob/wow/damn2/template/default.vm/media-type/html/role/guest/id/1234/language/de",
+                uri2.getAbsoluteLink() );
+        urlMapper.mapToURL( uri2 );
+        assertEquals( expectedMappedURL, uri2.getRelativeLink() );
+    }
+
+    @Test
+    public void testOverrideShortURL() throws Exception
+    {
+
+        PipelineData pipelineData = data;
+
+        assertNotNull( urlMapper );
+
+        ParameterParser pp = pipelineData.get( Turbine.class, ParameterParser.class );
+        assertNotNull( pp );
+        assertTrue( pp.keySet().isEmpty() );
+
+        pp.add( "role", "admin" ); // will not be overridden
+        urlMapper.mapFromURL( "/app/register", pp );
+
+        log.info( "parameters: {}", pp );
+        assertEquals( 4, pp.keySet().size() );
+        assertEquals( "random-id-123-abc", pp.getString( "js_pane" ) );
+        assertEquals( "admin", pp.getString( "role" ) );
+        assertEquals( "html", pp.getString( "media-type" ) );
+        assertEquals( "Registerone.vm", pp.getString( "template" ) );
+
+        TemplateURI uri2 = new TemplateURI( pipelineData.getRunData() );
+        uri2.clearResponse();
+        uri2.setTemplate( "Registerone.vm" );
+        pp.remove( "role" );
+        pp.add( "role", "anon" );
+        uri2.addPathInfo( pp );
+
+        // this is an artifical url, as the exact sequence could not be reconstructed as
+        // ParameterParser uses expicitely a random access table
+        assertEquals(
+                "scheme://bob/wow/damn2/template/Registerone.vm/media-type/html/js_pane/random-id-123-abc/role/anon",
+                uri2.getAbsoluteLink() );
+        urlMapper.mapToURL( uri2 );
+        String expectedMappedURL = "/wow/damn2/register";
+        assertEquals( expectedMappedURL, uri2.getRelativeLink() );
+
+        pp.clear();
+        pp.add( "role", "admin" );// will be overridden
+        urlMapper.mapFromURL( "/app/contact", pp );
+        log.info( "parameters: {}", pp );
+        assertEquals( 4, pp.keySet().size() );
+        assertEquals( "anon", pp.getString( "role" ) );
+        assertEquals( "another-random-id-876-dfg", pp.getString( "js_pane" ) );
+
+        uri2 = new TemplateURI( pipelineData.getRunData() );
+        uri2.clearResponse();
+        uri2.addPathInfo( pp );
+
+        // this is an artifical url
+        assertEquals( "scheme://bob/wow/damn2/page/Contact/media-type/html/js_pane/another-random-id-876-dfg/role/anon",
+                uri2.getAbsoluteLink() );
+        urlMapper.mapToURL( uri2 );
+        expectedMappedURL = "/wow/damn2/contact";
+        assertEquals( expectedMappedURL, uri2.getRelativeLink() );
+
+    }
+
+//	/**
+//	 * 		Not implemented Test for MappedTemplateLink:
+//	 * - To work with <i>MappedTemplateLink</i>, we need access to the urlmapperservice in order to 
+//	 * - simulate a request without pipeline (setting velocity context and initializing the service):
+//	 */
+//   @Test
+//   public void testMappedURILink() {   
+//   	MappedTemplateLink ml = MappedTemplateLink.class.getDeclaredConstructor().newInstance();
+//   	assertNotNull(ml);
+//   	ml.setUrlMapperService(urlMapper);
+//   	ml.init(data);
+//   }
+
+}
diff --git a/src/test/org/apache/turbine/services/urlmapper/TurbineYamlURLMapperServiceTest.java b/src/test/org/apache/turbine/services/urlmapper/TurbineYamlURLMapperServiceTest.java
deleted file mode 100644
index b5bdc7a..0000000
--- a/src/test/org/apache/turbine/services/urlmapper/TurbineYamlURLMapperServiceTest.java
+++ /dev/null
@@ -1,206 +0,0 @@
-package org.apache.turbine.services.urlmapper;
-
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you 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.
- */
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertTrue;
-import static org.mockito.Mockito.mock;
-
-import javax.servlet.ServletConfig;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-
-import org.apache.fulcrum.parser.ParameterParser;
-import org.apache.logging.log4j.LogManager;
-import org.apache.logging.log4j.Logger;
-import org.apache.turbine.Turbine;
-import org.apache.turbine.pipeline.PipelineData;
-import org.apache.turbine.services.TurbineServices;
-import org.apache.turbine.test.BaseTestCase;
-import org.apache.turbine.util.RunData;
-import org.apache.turbine.util.TurbineConfig;
-import org.apache.turbine.util.uri.TemplateURI;
-import org.junit.jupiter.api.AfterAll;
-import org.junit.jupiter.api.BeforeAll;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Tag;
-import org.junit.jupiter.api.Test;
-import org.mockito.Mockito;
-
-@Tag("yaml")
-public class TurbineYamlURLMapperServiceTest extends BaseTestCase {
-	
-	private static TurbineConfig tc = null;
-
-	private static URLMapperService urlMapper = null;
-
-	private RunData data;
-	
-	Logger log = LogManager.getLogger();
-
-	@BeforeAll
-	public static void setUp() throws Exception {
-		tc = new TurbineConfig(".", "/conf/test/TurbineYamlURLMapperServiceTest.properties");
-		tc.initialize();
-
-		urlMapper = (URLMapperService) TurbineServices.getInstance().getService(URLMapperService.SERVICE_NAME);
-	}
-
-	@AfterAll
-	public static void tearDown() throws Exception {
-		if (tc != null) {
-			tc.dispose();
-		}
-	}
-
-	@BeforeEach
-	public void init() throws Exception {
-
-		ServletConfig config = tc.getTurbine().getServletConfig();
-		// mock(ServletConfig.class);
-		HttpServletRequest request = getMockRequest();
-		HttpServletResponse response = mock(HttpServletResponse.class);
-
-		data = getRunData(request, response, config);
-		
-		Mockito.when(response.encodeURL(Mockito.anyString())).thenAnswer(invocation -> invocation.getArgument(0));
-	}
-
-	@Test
-	public void testMapToAnotherURL() throws Exception {
-
-		PipelineData pipelineData = data;
-
-		assertNotNull(urlMapper);
-
-		TemplateURI uri = new TemplateURI(pipelineData.getRunData());
-		uri.addPathInfo("id", 1234);
-		uri.addPathInfo("role", "guest");
-		uri.addPathInfo("language", "de");
-
-		String unMappedURL = uri.getAbsoluteLink(); // scheme://bob/wow/damn2/id/1234/role/guest
-
-		urlMapper.mapToURL(uri);
-		urlMapper.mapToURL(uri); // should be idempotent
-
-		String mappedLink = uri.getRelativeLink(); // wow/damn2/id/1234/role/guest
-		log.info(unMappedURL);
-		log.info(mappedLink);
-
-		String expectedMappedURL = "/wow/1234/guest/de";
-		String expectedRawURL = "scheme://bob/wow/damn2/id/1234/role/guest/language/de";
-		// raw url
-		assertEquals(expectedRawURL, unMappedURL);
-		assertEquals(expectedMappedURL, mappedLink);
-
-		ParameterParser pp = pipelineData.get(Turbine.class, ParameterParser.class);
-		assertNotNull(pp);
-		assertTrue(pp.keySet().isEmpty());
-		urlMapper.mapFromURL(mappedLink, pp);
-
-		assertEquals(5, pp.keySet().size());
-		assertEquals(1234, pp.getInt("id"));
-		assertEquals("guest", pp.getString("role"));
-		assertEquals("de", pp.getString("language"));
-		assertEquals("html", pp.getString("media-type"));
-
-		TemplateURI uri2 = new TemplateURI(pipelineData.getRunData());
-		uri2.clearResponse();
-		uri2.setTemplate("default.vm");
-		uri2.addPathInfo(pp);
-		// this is an artifical url
-		assertEquals("scheme://bob/wow/damn2/template/default.vm/media-type/html/role/guest/id/1234/language/de",
-				uri2.getAbsoluteLink());
-		urlMapper.mapToURL(uri2);
-		assertEquals(expectedMappedURL, uri2.getRelativeLink());
-	}
-
-	@Test
-	public void testOverrideShortURL() throws Exception {
-
-		PipelineData pipelineData = data;
-		
-		assertNotNull(urlMapper);
-
-		ParameterParser pp = pipelineData.get(Turbine.class, ParameterParser.class);
-		assertNotNull(pp);
-		assertTrue(pp.keySet().isEmpty());
-		
-		pp.add("role", "admin"); // will not be overridden
-		urlMapper.mapFromURL("/app/register", pp);
-
-		assertEquals(4, pp.keySet().size());
-		assertEquals("random-id-123-abc", pp.getString("js_pane"));
-		assertEquals("admin", pp.getString("role"));
-//        assertEquals("de", pp.getString("language"));
-		assertEquals("html", pp.getString("media-type"));
-		assertEquals("Registerone.vm", pp.getString("template"));
-
-		TemplateURI uri2 = new TemplateURI(pipelineData.getRunData());
-		uri2.clearResponse();
-		uri2.setTemplate("Registerone.vm");
-		pp.remove("role");
-		pp.add("role", "anon");
-		uri2.addPathInfo(pp);
-
-		// this is an artifical url, as the exact sequence could not be reconstructed as
-		// ParameterParser uses expicitely a random access table
-		assertEquals(
-				"scheme://bob/wow/damn2/template/Registerone.vm/media-type/html/js_pane/random-id-123-abc/role/anon",
-				uri2.getAbsoluteLink());
-		urlMapper.mapToURL(uri2);
-		String expectedMappedURL = "/wow/register";
-		assertEquals(expectedMappedURL, uri2.getRelativeLink());
-
-		pp.clear();
-		pp.add("role", "admin");// will be overridden
-		urlMapper.mapFromURL("/app/contact", pp);
-		assertEquals(4, pp.keySet().size());
-		assertEquals("anon", pp.getString("role"));
-		assertEquals("another-random-id-876-dfg", pp.getString("js_pane"));
-
-		uri2 = new TemplateURI(pipelineData.getRunData());
-		uri2.clearResponse();
-		uri2.addPathInfo(pp);
-
-		// this is an artifical url
-		assertEquals("scheme://bob/wow/damn2/page/Contact/media-type/html/js_pane/another-random-id-876-dfg/role/anon",
-				uri2.getAbsoluteLink());
-		urlMapper.mapToURL(uri2);
-		expectedMappedURL = "/wow/contact";
-		assertEquals(expectedMappedURL, uri2.getRelativeLink());
-
-	}
-	
-//	/**
-//	 * 		Not implemented Test for MappedTemplateLink:
-//	 * - To work with <i>MappedTemplateLink</i>, we need access to the urlmapperservice in order to 
-//	 * - simulate a request without pipeline (setting velocity context and initializing the service):
-//	 */
-//   @Test
-//   public void testMappedURILink() {   
-//   	MappedTemplateLink ml = MappedTemplateLink.class.getDeclaredConstructor().newInstance();
-//   	assertNotNull(ml);
-//   	ml.setUrlMapperService(urlMapper);
-//   	ml.init(data);
-//   }
-
-}
diff --git a/src/test/org/apache/turbine/services/urlmapper/model/YamlURLMappingContainerTest.java b/src/test/org/apache/turbine/services/urlmapper/model/YamlURLMappingContainerTest.java
index b35f310..89d4922 100644
--- a/src/test/org/apache/turbine/services/urlmapper/model/YamlURLMappingContainerTest.java
+++ b/src/test/org/apache/turbine/services/urlmapper/model/YamlURLMappingContainerTest.java
@@ -1,7 +1,9 @@
 package org.apache.turbine.services.urlmapper.model;
 
-
-import static org.junit.jupiter.api.Assertions.*;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 
 import java.io.FileInputStream;
 import java.io.InputStream;
@@ -24,43 +26,44 @@
     @BeforeAll
     public static void setUp() throws Exception
     {
-        try (InputStream reader = new FileInputStream("conf/turbine-url-mapping.yml"))
+        try (InputStream reader = new FileInputStream( "conf/turbine-url-mapping.yml" ))
         {
-        	ObjectMapper mapper = new ObjectMapper(new YAMLFactory());
-        	// List<URLMapEntry> urlList = 
-        	// mapper.readValue(reader, mapper.getTypeFactory().constructCollectionType(List.class, URLMapEntry.class));//            	
-        	container = mapper.readValue(reader, URLMappingContainer.class);
+            ObjectMapper mapper = new ObjectMapper( new YAMLFactory() );
+            // List<URLMapEntry> urlList =
+            // mapper.readValue(reader, mapper.getTypeFactory().constructCollectionType(List.class,
+            // URLMapEntry.class));//
+            container = mapper.readValue( reader, URLMappingContainer.class );
         }
     }
 
     @Test
     public void testGetName()
     {
-        assertNotNull(container);
-        assertEquals("default", container.getName());
+        assertNotNull( container );
+        assertEquals( "default", container.getName() );
     }
 
     @Test
     public void testGetMapEntries()
     {
-        assertNotNull(container);
+        assertNotNull( container );
 
         List<URLMapEntry> mapEntries = container.getMapEntries();
-        assertNotNull(mapEntries);
-        assertNotEquals(0, mapEntries.size());
+        assertNotNull( mapEntries );
+        assertNotEquals( 0, mapEntries.size() );
 
-        URLMapEntry entry = mapEntries.get(0);
-        assertNotNull(entry);
+        URLMapEntry entry = mapEntries.get( 0 );
+        assertNotNull( entry );
 
         Pattern pattern = entry.getUrlPattern();
-        assertNotNull(pattern);
-        assertTrue(pattern.matcher("/app/book/123").matches());
+        assertNotNull( pattern );
+        assertTrue( pattern.matcher( "/app/book/123" ).matches() );
 
         Map<String, String> implicit = entry.getImplicitParameters();
-        assertNotNull(implicit);
-        assertEquals(2, implicit.size());
-        assertEquals("Book.vm", implicit.get("template"));
-        assertEquals("0", implicit.get("detail"));
+        assertNotNull( implicit );
+        assertEquals( 2, implicit.size() );
+        assertEquals( "Book.vm", implicit.get( "template" ) );
+        assertEquals( "0", implicit.get( "detail" ) );
     }
 
 }
diff --git a/xdocs/howto/url-mapper-howto.xml b/xdocs/howto/url-mapper-howto.xml
new file mode 100644
index 0000000..61ac402
--- /dev/null
+++ b/xdocs/howto/url-mapper-howto.xml
@@ -0,0 +1,114 @@
+<?xml version="1.0"?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements.  See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership.  The ASF licenses this file
+ to you 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.
+-->
+
+<document>
+
+ <properties>
+  <title>URL Mapper Howto</title>
+ </properties>
+
+<body>
+
+<section>
+
+<p>
+Unaltered Turbine URLs look like this:
+<code>http://www.foo.com:8080/CONTEXT/servlet/MAPPING/template/Foo.vm</code>.<br/>
+But you want shorter URLs, or you don't like exposing the use of
+servlets in the URL.  Maybe this url would suit you better:
+<code>http://www.foo.com:8080/beautiful/world</code>
+</p>
+
+
+</section>
+
+<section name="Turbine Configuration">
+
+<p>
+You need to register the URL Mapper service in the pipeline by adding the service, the configuration 
+</p>
+
+<p>
+In TurbineResources.properties, search URLMapperService, and if not found, add the following settings:
+</p>
+
+<source><![CDATA[
+
+# -------------------------------------------------------------------
+#
+#  U R L  M A P P E R  S E R V I C E
+#
+# -------------------------------------------------------------------
+
+# new mapper
+tool.request.mlink=org.apache.turbine.services.urlmapper.MappedTemplateLink
+
+services.URLMapperService.classname=org.apache.turbine.services.urlmapper.TurbineURLMapperService
+
+# xml, json and yml supported as extension
+services.URLMapperService.configFile = /conf/turbine-url-mapping.xml
+
+]]></source>
+
+<p>Add the valve into pipeline (pipeline.default.descriptor = /conf/turbine-classic-pipeline.xml). 
+</p>
+
+<source><![CDATA[
+  <valves>
+    <valve>org.apache.turbine.services.urlmapper.URLMapperValve</valve>
+    ...
+
+]]></source>
+
+<p>This will read the beautfied URL and alter into to what, the server requires as defined 
+in the URLMapperService's configfile . 
+</p>
+
+<source><![CDATA[
+<url-mapping name="default">
+    <maps>
+        <map>
+            <pattern>/(?&lt;contextPath&gt;\w+)/book/(?&lt;bookId&gt;\d+)</pattern>
+            <implicit-parameters>
+                <parameter key="template">Book.vm</parameter>
+                <parameter key="detail">0</parameter>
+            </implicit-parameters>
+        </map>
+        ...
+]]></source>
+
+<p>
+Use it in the templates, e.g.
+</p>
+
+<source><![CDATA[
+    $mlink.addPathInfo("world","nice").getRelativeLink()
+    ## may result into /beautiful/world
+]]></source>
+
+<p>
+More examples ...
+</p>
+
+
+</section>
+
+</body>
+</document>