Merged revision(s) 1885148-1885758 from turbine/core/branches/URLMapperService:


git-svn-id: https://svn.apache.org/repos/asf/turbine/core/trunk@1885760 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/conf/test/TurbineURLMapperServiceTest.properties b/conf/test/TurbineURLMapperServiceTest.properties
new file mode 100644
index 0000000..53a36d3
--- /dev/null
+++ b/conf/test/TurbineURLMapperServiceTest.properties
@@ -0,0 +1,133 @@
+# 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.
+
+# -------------------------------------------------------------------
+#
+#  L O G 4 J 2 - L O G G I N G
+#
+# -------------------------------------------------------------------
+# log4j2 may loads automatically if found on classpath, cf. https://logging.apache.org/log4j/2.x
+log4j2.file = log4j2.xml
+
+# resource relative to context
+pipeline.default.descriptor = /conf/turbine-classic-pipeline.xml
+
+
+# If module.cache=true, then how large should we make the hashtables
+# by default.
+
+action.cache.size=20
+layout.cache.size=10
+navigation.cache.size=10
+page.cache.size=5
+screen.cache.size=50
+scheduledjob.cache.size=10
+
+# -------------------------------------------------------------------
+#
+#  M O D U L E  P A C K A G E S
+#
+# -------------------------------------------------------------------
+# This is the "classpath" for Turbine.  In order to locate your own
+# modules, you should add them to this path.  For example, if you have
+# com.company.actions, com.company.screens, com.company.navigations,
+# then this setting would be "com.company,org.apache.turbine.modules".
+# This path is searched in order.  For example, Turbine comes with a
+# screen module named "Login".  If you wanted to have your own screen
+# module named "Login", then you would specify the path to your
+# modules before the others.
+#
+# Note: org.apache.turbine.modules will always be added to the search
+# path.  If it is not explicitly added here, it will be added to the
+# end.
+#
+# Default: org.apache.turbine.modules
+# -------------------------------------------------------------------
+
+module.packages=@MODULE_PACKAGES@
+
+# Choose between the two available implementations of an Avalon container - ECM or YAAFI
+
+# services.AvalonComponentService.classname=org.apache.turbine.services.avaloncomponent.TurbineAvalonComponentService
+services.AvalonComponentService.classname=org.apache.turbine.services.avaloncomponent.TurbineYaafiComponentService
+
+services.RunDataService.classname=org.apache.turbine.services.rundata.TurbineRunDataService
+services.AssemblerBrokerService.classname=org.apache.turbine.services.assemblerbroker.TurbineAssemblerBrokerService
+services.TemplateService.classname=org.apache.turbine.services.template.TurbineTemplateService
+
+# required by url mapper service
+services.ServletService.classname=org.apache.turbine.services.servlet.TurbineServletService
+
+services.URLMapperService.classname=org.apache.turbine.services.urlmapper.TurbineURLMapperService
+
+# -------------------------------------------------------------------
+#
+#  R U N D A T A  S E R V I C E
+#
+# -------------------------------------------------------------------
+
+services.RunDataService.default.run.data=org.apache.turbine.services.rundata.DefaultTurbineRunData
+services.RunDataService.default.parameter.parser=org.apache.fulcrum.parser.DefaultParameterParser
+services.RunDataService.default.cookie.parser=org.apache.fulcrum.parser.DefaultCookieParser
+
+# -------------------------------------------------------------------
+#
+#  A S S E M B L E R  B R O K E R  S E R V I C E
+#
+# -------------------------------------------------------------------
+# A list of AssemblerFactory classes that will be registered
+# with TurbineAssemblerBrokerService
+# -------------------------------------------------------------------
+
+services.AssemblerBrokerService.screen=org.apache.turbine.services.assemblerbroker.util.java.JavaScreenFactory
+# services.AssemblerBrokerService.screen=org.apache.turbine.services.assemblerbroker.util.python.PythonScreenFactory
+services.AssemblerBrokerService.action=org.apache.turbine.services.assemblerbroker.util.java.JavaActionFactory
+services.AssemblerBrokerService.layout=org.apache.turbine.services.assemblerbroker.util.java.JavaLayoutFactory
+services.AssemblerBrokerService.page=org.apache.turbine.services.assemblerbroker.util.java.JavaPageFactory
+services.AssemblerBrokerService.navigation=org.apache.turbine.services.assemblerbroker.util.java.JavaNavigationFactory
+services.AssemblerBrokerService.scheduledjob=org.apache.turbine.services.assemblerbroker.util.java.JavaScheduledJobFactory
+
+# -------------------------------------------------------------------
+#
+#  T E M P L A T E  S E R V I C E
+#
+# -------------------------------------------------------------------
+
+# Roughly, the number of templates in each category.
+#
+# Defaults: layout=2, navigation=10, screen=50
+
+services.TemplateService.layout.cache.size=2
+services.TemplateService.navigation.cache.size=10
+services.TemplateService.screen.cache.size=50
+
+# -------------------------------------------------------------------
+#
+#  A V A L O N C O M P O N E N T  S E R V I C E
+#
+# -------------------------------------------------------------------
+
+services.AvalonComponentService.componentConfiguration = conf/test/fulcrumComponentConfiguration.xml
+services.AvalonComponentService.componentRoles = conf/test/fulcrumRoleConfiguration.xml
+services.AvalonComponentService.lookup = org.apache.fulcrum.cache.GlobalCacheService
+
+# -------------------------------------------------------------------
+#
+#  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/fulcrumComponentConfiguration.xml b/conf/test/fulcrumComponentConfiguration.xml
index 7a1c009..c45ee5b 100644
--- a/conf/test/fulcrumComponentConfiguration.xml
+++ b/conf/test/fulcrumComponentConfiguration.xml
@@ -53,8 +53,15 @@
     <parser>
         <parameterEncoding>utf-8</parameterEncoding>
         <automaticUpload>true</automaticUpload>
+        <pool2>
+           <!--  cft. defaults in org.apache.commons.pool2.impl.BaseObjectPoolConfig and GenericKeyedObjectPoolConfig -->
+           <maxTotal>-1</maxTotal><!--  default no limit = -1,  other plausible values 1024, 2048 -->
+           <blockWhenExhausted>true</blockWhenExhausted><!--  default true -->
+           <maxWaitMillis>350</maxWaitMillis><!--  default 0 -->
+           <testOnReturn>true</testOnReturn>
+        </pool2>
     </parser>
-
+    
     <!-- These components belong to the Fulcrum-Security services -->
     <securityService/>
     <authenticator/>
diff --git a/conf/test/log4j2.xml b/conf/test/log4j2.xml
index dfe68ac..99529bd 100644
--- a/conf/test/log4j2.xml
+++ b/conf/test/log4j2.xml
@@ -30,15 +30,18 @@
     <Loggers>
          <Logger name="org.apache.turbine" level="debug" additivity="false">
           <AppenderRef ref="logfile"/>
+          <AppenderRef ref="console"/>
         </Logger>
         <Logger name="avalon" level="info" additivity="false">
 	      <AppenderRef ref="logfile"/>
+	      <AppenderRef ref="console"/>
 	    </Logger>
       <Logger name="org.apache.logging.log4j" level="debug" additivity="false">
 	      <AppenderRef ref="logfile"/>
-	    </Logger>
-	    <Root level="error">
-	      <AppenderRef ref="logfile"/>
-	    </Root>
+	  </Logger>
+	  <Root>
+	      <AppenderRef ref="logfile" level="error"/>
+	      <AppenderRef ref="console" level="debug"/>
+	  </Root>
     </Loggers>
 </Configuration>
\ No newline at end of file
diff --git a/conf/turbine-url-mapping.xml b/conf/turbine-url-mapping.xml
new file mode 100644
index 0000000..d172e18
--- /dev/null
+++ b/conf/turbine-url-mapping.xml
@@ -0,0 +1,67 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ 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.
+-->
+<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>
+        <map>
+            <pattern>/(?&lt;contextPath&gt;\w+)/book/(?&lt;bookId&gt;\d+)/(?&lt;detail&gt;\d)</pattern>
+            <implicit-parameters>
+                <parameter key="template">Book.vm</parameter>
+            </implicit-parameters>
+            <ignore-parameters>
+                <parameter key="view" />
+            </ignore-parameters>
+        </map>
+        <map>
+            <pattern>/(?&lt;contextPath&gt;\w+)/register</pattern>
+            <implicit-parameters>
+                <parameter key="media-type">html</parameter>
+                <parameter key="role">anon</parameter>
+                <parameter key="template">Registerone.vm</parameter>
+                <parameter key="js_pane">random-id-123-abc</parameter>
+            </implicit-parameters>
+        </map>
+        <map>
+            <pattern>/(?&lt;contextPath&gt;\w+)/contact</pattern>
+            <implicit-parameters>
+                <parameter key="media-type">html</parameter>
+                <parameter key="role">anon</parameter>
+                <parameter key="page">Contact</parameter>
+                <parameter key="js_pane">another-random-id-876-dfg</parameter>
+            </implicit-parameters>
+            <override-parameters>
+                <parameter key="role">anon</parameter>
+            </override-parameters>
+        </map>
+        <map>
+            <pattern>/(?&lt;contextPath&gt;\w+)/(?&lt;id&gt;\d+)/(?&lt;role&gt;\w+)/(?&lt;language&gt;\w+)</pattern>
+            <implicit-parameters>
+                <parameter key="media-type">html</parameter>
+                <parameter key="template">default.vm</parameter>
+            </implicit-parameters>
+        </map>
+    </maps>
+</url-mapping>
\ No newline at end of file
diff --git a/conf/turbine-url-mapping.yml b/conf/turbine-url-mapping.yml
new file mode 100644
index 0000000..6a47d64
--- /dev/null
+++ b/conf/turbine-url-mapping.yml
@@ -0,0 +1,31 @@
+
+name: default
+maps:
+    - pattern: /(?<contextPath>\w+)/book/(?<bookId>\d+)
+      implicit-parameters:
+        template: Book.vm
+        detail: 0      
+    - pattern: /(?<contextPath>\w+)/book/(?<bookId>\d+)/(?<detail>\d)
+      implicit-parameters:
+        template: Book.vm
+      ignore-parameters:
+        view: null
+    - pattern: /(?<contextPath>\w+)/register
+      implicit-parameters:
+        media-type: html
+        role: anon
+        template: Registerone.vm
+        js_pane: random-id-123-abc
+    - pattern: /(?<contextPath>\w+)/contact
+      implicit-parameters:
+        media-type: html
+        page: Contact
+        js_pane: another-random-id-876-dfg       
+        role: anon
+      override-parameters:
+        role: anon
+    - pattern: /(?<contextPath>\w+)/(?<id>\d+)/(?<role>\w+)/(?<language>\w+)
+      implicit-parameters:
+        media-type: html
+        template: default.vm    
+                
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index 7178da7..6aa42d7 100644
--- a/pom.xml
+++ b/pom.xml
@@ -22,7 +22,7 @@
   <parent>

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

     <artifactId>turbine-parent</artifactId>

-    <version>8-SNAPSHOT</version>

+    <version>7</version>

   </parent>

   <artifactId>turbine</artifactId>

   <name>Apache Turbine</name>

@@ -547,14 +547,18 @@
       <plugin>

         <groupId>org.apache.maven.plugins</groupId>

         <artifactId>maven-surefire-plugin</artifactId>

-         <version>3.0.0-M5</version>

-        <configuration>

-          <!--default setting is forkCount=1/reuseForks=true -->

-          <reuseForks>false</reuseForks>

-          <forkCount>1</forkCount>

-          <excludedGroups>performance,docker</excludedGroups>

-          <!--useSystemClassLoader>false</useSystemClassLoader-->

-        </configuration>

+        <version>3.0.0-M5</version>

+        <executions>

+                <execution>

+                   <id>default-test</id>

+                    <configuration>

+                      <!--default setting is forkCount=1/reuseForks=true -->

+                      <reuseForks>false</reuseForks>

+                      <forkCount>1</forkCount>

+                      <excludedGroups>performance,docker,yaml</excludedGroups>

+                    </configuration>

+             </execution>

+         </executions>

       </plugin>

       <plugin>

         <groupId>org.apache.torque</groupId>

@@ -888,6 +892,12 @@
       <version>1.9.4</version>

     </dependency>

     <dependency>

+      <groupId>com.fasterxml.jackson.dataformat</groupId>

+      <artifactId>jackson-dataformat-yaml</artifactId>

+      <version>2.11.2</version>

+      <optional>true</optional>

+    </dependency>

+    <dependency>

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

       <artifactId>yauaa</artifactId>

       <version>5.19</version>

@@ -1181,7 +1191,7 @@
                    <id>default-test</id>

                   <configuration>

                     <groups>docker</groups>

-                    <excludedGroups>performance</excludedGroups>

+                    <excludedGroups>performance,yaml</excludedGroups>

                   </configuration>

                 </execution>

               </executions>

@@ -1237,6 +1247,37 @@
           </dependency>

       </dependencies>

     </profile>

+    <profile>

+        <id>yaml</id>

+      <activation>

+        <activeByDefault>false</activeByDefault>

+      </activation>

+      <build>

+      <plugins>

+            <plugin>

+              <groupId>org.apache.maven.plugins</groupId>

+              <artifactId>maven-surefire-plugin</artifactId>

+               <version>3.0.0-M5</version>

+              <executions>

+                <execution>

+                   <id>default-test</id>

+                  <configuration><!-- to override excludedGroups set something else (bug?)-->

+                    <groups>yaml</groups>

+                     <excludedGroups>docker,performance</excludedGroups>

+                  </configuration>

+                </execution>

+              </executions>

+            </plugin>

+          </plugins>

+      </build>

+      <dependencies>

+           <dependency>

+               <groupId>com.fasterxml.jackson.dataformat</groupId>

+               <artifactId>jackson-dataformat-yaml</artifactId>

+               <version>2.11.2</version>

+           </dependency>

+       </dependencies>

+    </profile>

   </profiles>

 

   <properties>

@@ -1255,3 +1296,4 @@
   </properties>

 

 </project>

+

diff --git a/src/java/org/apache/turbine/services/pull/TurbinePullService.java b/src/java/org/apache/turbine/services/pull/TurbinePullService.java
index 00e1537..c2be3bf 100644
--- a/src/java/org/apache/turbine/services/pull/TurbinePullService.java
+++ b/src/java/org/apache/turbine/services/pull/TurbinePullService.java
@@ -466,7 +466,7 @@
         {
             try
             {
-                Object tool = toolData.toolClass.newInstance();
+                Object tool = toolData.toolClass.getDeclaredConstructor().newInstance();
 
                 // global tools are init'd with a null data parameter
                 initTool(tool, null);
diff --git a/src/java/org/apache/turbine/services/pull/tools/TemplateLink.java b/src/java/org/apache/turbine/services/pull/tools/TemplateLink.java
index e870ade..122226a 100644
--- a/src/java/org/apache/turbine/services/pull/tools/TemplateLink.java
+++ b/src/java/org/apache/turbine/services/pull/tools/TemplateLink.java
@@ -520,7 +520,7 @@
      * tui.getRelativeLink();
      * </pre>
      *
-     * The above call to absoluteLink() would return the String:
+     * The above call to relativeLink() would return the String:
      * <p>
      * /servlets/Turbine/screen/UserScreen/user/jon
      * <p>
diff --git a/src/java/org/apache/turbine/services/urlmapper/MappedTemplateLink.java b/src/java/org/apache/turbine/services/urlmapper/MappedTemplateLink.java
new file mode 100644
index 0000000..ea2b1c1
--- /dev/null
+++ b/src/java/org/apache/turbine/services/urlmapper/MappedTemplateLink.java
@@ -0,0 +1,116 @@
+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 org.apache.turbine.annotation.TurbineService;
+import org.apache.turbine.services.pull.tools.TemplateLink;
+
+/**
+ * This is a pull to to be used in Templates to convert links in
+ * Templates into the correct references.
+ *
+ * The pull service might insert this tool into the Context.
+ * in templates.  Here's an example of its Velocity use:
+ *
+ * <p><code>
+ * $link.setPage("index.vm").addPathInfo("hello","world")
+ * This would return: http://foo.com/Turbine/template/index.vm/hello/world
+ * </code>
+ *
+ * <p>
+ *
+ * This is an application pull tool for the template system. You should <b>not</b>
+ * use it in a normal application!
+ *
+ * @author <a href="mbryson@mont.mindspring.com">Dave Bryson</a>
+ * @author <a href="mailto:jon@latchkey.com">Jon S. Stevens</a>
+ * @author <a href="mailto:hps@intermeta.de">Henning P. Schmiedehausen</a>
+ * @author <a href="mailto:quintonm@bellsouth.net">Quinton McCombs</a>
+ * @author <a href="mailto:peter@courcoux.biz">Peter Courcoux</a>
+ * @version $Id: TemplateLink.java 1854688 2019-03-03 10:36:42Z tv $
+ */
+
+public class MappedTemplateLink extends TemplateLink
+{
+    /**
+     * The URL Mapper service.
+     */
+    @TurbineService
+    private URLMapperService urlMapperService;
+    
+
+    /**
+     * Builds the URL with all of the data URL-encoded as well as
+     * encoded using HttpServletResponse.encodeUrl(). The resulting
+     * URL is absolute; it starts with http/https...
+     *
+     * <pre>
+     * TemplateURI tui = new TemplateURI (data, "UserScreen");
+     * tui.addPathInfo("user","jon");
+     * tui.getAbsoluteLink();
+     * </pre>
+     *
+     * The above call to absoluteLink() would return the String:
+     * <p>
+     * http://www.server.com/servlets/Turbine/screen/UserScreen/user/jon
+     * <p>
+     * After rendering the URI, it clears the
+     * pathInfo and QueryString portions of the TemplateURI. So you can
+     * use the $link reference multiple times on a page and start over
+     * with a fresh object every time.
+     *
+     * @return A String with the built URL.
+     */
+    public String getAbsoluteLink()
+    {
+        urlMapperService.mapToURL(templateURI);
+        return super.getAbsoluteLink();
+    }
+
+
+    /**
+     * Builds the URL with all of the data URL-encoded as well as
+     * encoded using HttpServletResponse.encodeUrl(). The resulting
+     * URL is relative to the webserver root.
+     *
+     * <pre>
+     * TemplateURI tui = new TemplateURI (data, "UserScreen");
+     * tui.addPathInfo("user","jon");
+     * tui.getRelativeLink();
+     * </pre>
+     *
+     * The above call to relativeLink() would return the String:
+     * <p>
+     * /servlets/Turbine/screen/UserScreen/user/jon
+     * <p>
+     * After rendering the URI, it clears the
+     * pathInfo and QueryString portions of the TemplateURI. So you can
+     * use the $link reference multiple times on a page and start over
+     * with a fresh object every time.
+     *
+     * @return A String with the built URL.
+     */
+    public String getRelativeLink()
+    {
+        urlMapperService.mapToURL(templateURI);
+        return super.getRelativeLink();
+    }
+    
+}
diff --git a/src/java/org/apache/turbine/services/urlmapper/TurbineURLMapperService.java b/src/java/org/apache/turbine/services/urlmapper/TurbineURLMapperService.java
new file mode 100644
index 0000000..dc79c2f
--- /dev/null
+++ b/src/java/org/apache/turbine/services/urlmapper/TurbineURLMapperService.java
@@ -0,0 +1,319 @@
+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 java.io.IOException;
+import java.io.InputStream;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import javax.xml.bind.JAXBContext;
+import javax.xml.bind.JAXBException;
+import javax.xml.bind.Unmarshaller;
+
+import org.apache.commons.configuration2.Configuration;
+import org.apache.fulcrum.parser.ParameterParser;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.apache.turbine.services.InitializationException;
+import org.apache.turbine.services.TurbineBaseService;
+import org.apache.turbine.services.TurbineServices;
+import org.apache.turbine.services.servlet.ServletService;
+import org.apache.turbine.services.urlmapper.model.URLMapEntry;
+import org.apache.turbine.services.urlmapper.model.URLMappingContainer;
+import org.apache.turbine.util.uri.TurbineURI;
+import org.apache.turbine.util.uri.URIParam;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
+
+/**
+ * The URL mapper service provides methods to map a set of parameters to a
+ * simplified URL and vice-versa. This service was inspired by the
+ * Liferay Friendly URL Mapper.
+ * <p>
+ * A mapper valve and a link pull tool are provided for easy application.
+ *
+ * @author <a href="mailto:tv@apache.org">Thomas Vandahl</a>
+ * @see URLMapperService
+ * @see MappedTemplateLink
+ * @see URLMapperValve
+ *
+ * @version $Id$
+ */
+public class TurbineURLMapperService
+        extends TurbineBaseService
+        implements URLMapperService
+{
+    /**
+     * Logging.
+     */
+    private static final Logger log = LogManager.getLogger(TurbineURLMapperService.class);
+
+    /**
+     * The default configuration file.
+     */
+    private static final String DEFAULT_CONFIGURATION_FILE = "/WEB-INF/conf/turbine-url-mapping.xml";
+
+    /**
+     * The configuration key for the configuration file.
+     */
+    private static final String CONFIGURATION_FILE_KEY = "configFile";
+
+    /**
+     * The container with the URL mappings.
+     */
+    private URLMappingContainer container;
+
+    /**
+     * Regex pattern for group names
+     */
+    private static final Pattern namedGroupsPattern = Pattern.compile("\\(\\?<([a-zA-Z][a-zA-Z0-9]*)>.+?\\)");
+
+    /**
+     * Symbolic group name for context path
+     */
+    private static final String CONTEXT_PATH_PARAMETER = "contextPath";
+
+    /**
+     * Symbolic group name for web application root
+     */
+    private static final String WEBAPP_ROOT_PARAMETER = "webAppRoot";
+
+    /**
+     * Symbolic group names that will not be added to parameters
+     */
+    private static final Set<String> DEFAULT_PARAMETERS = Stream.of(
+            CONTEXT_PATH_PARAMETER,
+            WEBAPP_ROOT_PARAMETER
+    ).collect(Collectors.toSet());
+
+    /**
+     * Map a set of parameters (contained in TurbineURI PathInfo and QueryData)
+     * to a TurbineURI
+     *
+     * @param uri the URI to be modified (with setScriptName())
+     */
+    @Override
+    public void mapToURL(TurbineURI uri)
+    {
+        // Create map from list, taking only the first appearance of a key
+        // PathInfo takes precedence
+        Map<String, Object> uriParameterMap = Stream.concat(
+                uri.getPathInfo().stream(),
+                uri.getQueryData().stream())
+                .collect(Collectors.toMap(
+                        URIParam::getKey,
+                        URIParam::getValue,
+                        (e1, e2) -> e1,
+                        LinkedHashMap::new));
+
+        Set<String> keys = new HashSet<>(uriParameterMap.keySet());
+
+        if (keys.isEmpty() && uri.getQueryData().isEmpty() || uri.getPathInfo().isEmpty())
+        {
+            return; // no mapping or mapping already done
+        }
+
+        for (URLMapEntry urlMap : container.getMapEntries())
+        {
+            Set<String> entryKeys = new HashSet<>();
+
+            Map<String, Integer> groupNamesMap = urlMap.getGroupNamesMap();
+            if (groupNamesMap != null)
+            {
+                entryKeys.addAll(groupNamesMap.keySet());
+            }
+
+            Set<String> implicitKeysFound = urlMap.getImplicitParameters().entrySet().stream()
+                    .filter(entry -> Objects.equals(uriParameterMap.get(entry.getKey()), entry.getValue()))
+                    .map(Map.Entry::getKey)
+                    .collect(Collectors.toSet());
+
+            entryKeys.addAll(implicitKeysFound);
+            implicitKeysFound.forEach(key -> {
+                uri.removePathInfo(key);
+                uri.removeQueryData(key);
+            });
+
+            keys.removeAll(urlMap.getIgnoreParameters().keySet());
+
+            if (entryKeys.containsAll(keys))
+            {
+                Matcher matcher = namedGroupsPattern.matcher(urlMap.getUrlPattern().pattern());
+                StringBuffer sb = new StringBuffer();
+
+                while (matcher.find())
+                {
+                    String key = matcher.group(1);
+
+                    if (CONTEXT_PATH_PARAMETER.equals(key))
+                    {
+                        // remove
+                        matcher.appendReplacement(sb, "");
+                    } else if (WEBAPP_ROOT_PARAMETER.equals(key))
+                    {
+                        matcher.appendReplacement(sb, uri.getScriptName());
+                    } else
+                    {
+                        matcher.appendReplacement(sb,
+                                Matcher.quoteReplacement(
+                                        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("//", "/"));
+                break;
+            }
+        }
+    }
+
+    /**
+     * Map a simplified URL to a set of parameters
+     *
+     * @param url the URL
+     * @param pp  a ParameterParser to use for parameter mangling
+     */
+    @Override
+    public void mapFromURL(String url, ParameterParser pp)
+    {
+        for (URLMapEntry urlMap : container.getMapEntries())
+        {
+            Matcher matcher = urlMap.getUrlPattern().matcher(url);
+            if (matcher.matches())
+            {
+                // extract parameters from URL
+                Map<String, Integer> groupNameMap = urlMap.getGroupNamesMap();
+
+                if (groupNameMap != null)
+                {
+                    groupNameMap.entrySet().stream()
+                            // ignore default parameters
+                            .filter(group -> !DEFAULT_PARAMETERS.contains(group.getKey()))
+                            .forEach(group ->
+                                    pp.setString(group.getKey(), matcher.group(group.getValue().intValue())));
+                }
+
+                // add implicit parameters
+                urlMap.getImplicitParameters().entrySet().forEach(e ->
+                        pp.add(e.getKey(), e.getValue()));
+
+                // add override parameters
+                urlMap.getOverrideParameters().entrySet().forEach(e ->
+                        pp.setString(e.getKey(), e.getValue()));
+
+                // remove ignore parameters
+                urlMap.getIgnoreParameters().keySet().forEach(k ->
+                        pp.remove(k));
+
+                break;
+            }
+        }
+    }
+
+    // ---- Service initialization ------------------------------------------
+
+    /**
+     * Initializes the service.
+     */
+    @Override
+    public void init() throws InitializationException
+    {
+        Configuration cfg = getConfiguration();
+
+        String configFile = cfg.getString(CONFIGURATION_FILE_KEY, DEFAULT_CONFIGURATION_FILE);
+
+        // context resource path has to begin with slash, cft.
+        // context.getResource
+        if (!configFile.startsWith("/"))
+        {
+            configFile = "/" + configFile;
+        }
+
+        ServletService servletService = (ServletService) TurbineServices.getInstance().getService(ServletService.SERVICE_NAME);
+
+        try (InputStream reader = servletService.getResourceAsStream(configFile))
+        {
+            if (configFile.endsWith(".xml"))
+            {
+                JAXBContext jaxb = JAXBContext.newInstance(URLMappingContainer.class);
+                Unmarshaller unmarshaller = jaxb.createUnmarshaller();
+                container = (URLMappingContainer) unmarshaller.unmarshal(reader);
+            } else if (configFile.endsWith(".yml"))
+            {
+                // org.apache.commons.configuration2.YAMLConfiguration does only expose property like configuration values,
+                // which is not what we need here -> java object deserialization.
+                ObjectMapper mapper = new ObjectMapper(new YAMLFactory());
+                container = mapper.readValue(reader, URLMappingContainer.class);
+            }
+        }
+        catch (IOException | JAXBException e)
+        {
+            throw new InitializationException("Could not load configuration file " + configFile, e);
+        }
+
+        // Get groupNamesMap for every Pattern and store it in the entry
+        try
+        {
+            Method namedGroupsMethod = Pattern.class.getDeclaredMethod("namedGroups");
+            namedGroupsMethod.setAccessible(true);
+
+            for (URLMapEntry urlMap : container.getMapEntries())
+            {
+                @SuppressWarnings("unchecked")
+                Map<String, Integer> groupNamesMap = (Map<String, Integer>) namedGroupsMethod.invoke(urlMap.getUrlPattern());
+                urlMap.setGroupNamesMap(groupNamesMap);
+            }
+        }
+        catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException
+                | NoSuchMethodException | SecurityException e)
+        {
+            throw new InitializationException("Could not invoke method Pattern.getNamedGroups", e);
+        }
+
+        log.info("Loaded {} url-mappings from {}", Integer.valueOf(container.getMapEntries().size()), configFile);
+
+        setInit(true);
+    }
+
+    /**
+     * Returns to uninitialized state.
+     */
+    @Override
+    public void shutdown()
+    {
+        container = null;
+        setInit(false);
+    }
+}
diff --git a/src/java/org/apache/turbine/services/urlmapper/URLMapperService.java b/src/java/org/apache/turbine/services/urlmapper/URLMapperService.java
new file mode 100644
index 0000000..98e9b2d
--- /dev/null
+++ b/src/java/org/apache/turbine/services/urlmapper/URLMapperService.java
@@ -0,0 +1,58 @@
+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 org.apache.fulcrum.parser.ParameterParser;
+import org.apache.turbine.services.Service;
+import org.apache.turbine.util.uri.TurbineURI;
+
+/**
+ * The URL mapper service provides methods to map a set of parameters to a
+ * simplified URL and vice-versa. This service was inspired by the
+ * Liferay Friendly URL Mapper.
+ *
+ * A mapper valve and a link pull tool are provided for easy application.
+ *
+ * @author <a href="mailto:tv@apache.org">Thomas Vandahl</a>
+ */
+public interface URLMapperService extends Service
+{
+    /**
+     * The service identifier.
+     */
+    String SERVICE_NAME = "URLMapperService";
+
+    /**
+     * Map a set of parameters (contained in TurbineURI PathInfo and QueryData)
+     * to a TurbineURI
+     *
+     * @param uri the URI to be modified (with setScriptName())
+     */
+    void mapToURL(TurbineURI uri);
+
+    /**
+     * Map a simplified URL to a set of parameters
+     *
+     * @param url the URL
+     * @param pp a ParameterParser to use for parameter mangling
+     */
+    void mapFromURL(String url, ParameterParser pp);
+}
diff --git a/src/java/org/apache/turbine/services/urlmapper/URLMapperValve.java b/src/java/org/apache/turbine/services/urlmapper/URLMapperValve.java
new file mode 100644
index 0000000..e81b3c1
--- /dev/null
+++ b/src/java/org/apache/turbine/services/urlmapper/URLMapperValve.java
@@ -0,0 +1,61 @@
+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 java.io.IOException;
+
+import org.apache.turbine.annotation.TurbineService;
+import org.apache.turbine.pipeline.PipelineData;
+import org.apache.turbine.pipeline.Valve;
+import org.apache.turbine.pipeline.ValveContext;
+import org.apache.turbine.util.RunData;
+import org.apache.turbine.util.TurbineException;
+
+/**
+ * This valve is responsible for parsing parameters out of
+ * simplified URLs.
+ *
+ * @author <a href="mailto:tv@apache.org">Thomas Vandahl</a>
+ */
+public class URLMapperValve
+    implements Valve
+{
+    /** Injected service instance */
+    @TurbineService
+    private URLMapperService urlMapperService;
+
+    /**
+     * @see org.apache.turbine.pipeline.Valve#invoke(PipelineData, ValveContext)
+     */
+    @Override
+    public void invoke(PipelineData pipelineData, ValveContext context)
+        throws IOException, TurbineException
+    {
+        RunData data = pipelineData.getRunData();
+        String uri = data.getRequest().getRequestURI();
+
+        urlMapperService.mapFromURL(uri, data.getParameters());
+
+        // Pass control to the next Valve in the Pipeline
+        context.invokeNext(pipelineData);
+    }
+}
diff --git a/src/java/org/apache/turbine/services/urlmapper/model/URLMapEntry.java b/src/java/org/apache/turbine/services/urlmapper/model/URLMapEntry.java
new file mode 100644
index 0000000..ccb4d3f
--- /dev/null
+++ b/src/java/org/apache/turbine/services/urlmapper/model/URLMapEntry.java
@@ -0,0 +1,175 @@
+package org.apache.turbine.services.urlmapper.model;
+
+/*
+ * 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 java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Pattern;
+
+import javax.xml.bind.annotation.XmlAccessType;
+import javax.xml.bind.annotation.XmlAccessorType;
+import javax.xml.bind.annotation.XmlElement;
+import javax.xml.bind.annotation.XmlType;
+import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * The url map model class
+ *
+ * @author <a href="mailto:tv@apache.org">Thomas Vandahl</a>
+ */
+@XmlType(name="map")
+@XmlAccessorType(XmlAccessType.NONE)
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class URLMapEntry
+{
+    private Pattern urlPattern;
+    private Map<String, String> implicit = new LinkedHashMap<>();
+    private Map<String, String> ignore = new LinkedHashMap<>();
+    private Map<String, String> override = new LinkedHashMap<>();
+
+    private Map<String, Integer> groupNamesMap;
+    private Set<String> relevantKeys = null;
+
+    /**
+     * @return the urlPattern
+     */
+    @XmlElement(name="pattern")
+    @XmlJavaTypeAdapter(XmlPatternAdapter.class)
+    @JsonProperty("pattern")
+    public Pattern getUrlPattern()
+    {
+        return urlPattern;
+    }
+
+    /**
+     * @param urlPattern the urlPattern to set
+     */
+    protected void setUrlPattern(Pattern urlPattern)
+    {
+        this.urlPattern = urlPattern;
+    }
+
+    /**
+     * @return the implicit parameters
+     */
+    @XmlElement(name="implicit-parameters")
+    @XmlJavaTypeAdapter(XmlParameterAdapter.class)
+    @JsonProperty("implicit-parameters")
+    public Map<String, String> getImplicitParameters()
+    {
+        return implicit;
+    }
+
+    /**
+     * @param implicit the implicit parameters to set
+     */
+    protected void setImplicitParameters(Map<String, String> implicit)
+    {
+        this.implicit = implicit;
+    }
+
+    /**
+     * @return the ignored parameters
+     */
+    @XmlElement(name="ignore-parameters")
+    @XmlJavaTypeAdapter(XmlParameterAdapter.class)
+    @JsonProperty("ignore-parameters")
+    public Map<String, String> getIgnoreParameters()
+    {
+        return ignore;
+    }
+
+    /**
+     * @param ignore the ignored parameters to set
+     */
+    protected void setIgnoreParameters(Map<String, String> ignore)
+    {
+        this.ignore = ignore;
+    }
+
+    /**
+     * @return the override parameters
+     */
+    @XmlElement(name="override-parameters")
+    @XmlJavaTypeAdapter(XmlParameterAdapter.class)
+    @JsonProperty("override-parameters")
+    public Map<String, String> getOverrideParameters()
+    {
+        return override;
+    }
+
+    /**
+     * @param override the override parameters to set
+     */
+    protected void setOverrideParameters(Map<String, String> override)
+    {
+        this.override = override;
+    }
+
+    /**
+     * Get the map of group names to group indices for the stored Pattern
+     *
+     * @return the groupNamesMap
+     */
+    public Map<String, Integer> getGroupNamesMap()
+    {
+        return groupNamesMap;
+    }
+
+    /**
+     * Set the map of group names to group indices for the stored Pattern
+     *
+     * @param groupNamesMap the groupNamesMap to set
+     */
+    public void setGroupNamesMap(Map<String, Integer> groupNamesMap)
+    {
+        this.groupNamesMap = groupNamesMap;
+    }
+
+    /**
+     * Get the set of relevant keys for comparison (cached for performance)
+     *
+     * @return the relevantKeys
+     */
+    public Set<String> getRelevantKeys()
+    {
+        return relevantKeys;
+    }
+
+    /**
+     * Set the set of relevant keys for comparison (cached for performance)
+     *
+     * @param relevantKeys the relevantKeys to set
+     */
+    public void setRelevantKeys(Set<String> relevantKeys)
+    {
+        this.relevantKeys = relevantKeys;
+    }
+
+    @Override
+    public String toString()
+    {
+    	return "URLMapEntry: [ pattern: " + urlPattern + ", implicit-parameters: " + implicit + ", override-parameters: " + override + ", ignore-parameters:" + ignore + " ]";
+    }
+}
diff --git a/src/java/org/apache/turbine/services/urlmapper/model/URLMappingContainer.java b/src/java/org/apache/turbine/services/urlmapper/model/URLMappingContainer.java
new file mode 100644
index 0000000..9da6915
--- /dev/null
+++ b/src/java/org/apache/turbine/services/urlmapper/model/URLMappingContainer.java
@@ -0,0 +1,98 @@
+package org.apache.turbine.services.urlmapper.model;
+
+/*
+ * 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 java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+import javax.xml.bind.annotation.XmlAccessType;
+import javax.xml.bind.annotation.XmlAccessorType;
+import javax.xml.bind.annotation.XmlAttribute;
+import javax.xml.bind.annotation.XmlElement;
+import javax.xml.bind.annotation.XmlElementWrapper;
+import javax.xml.bind.annotation.XmlRootElement;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * URL Map Container Model Class
+ *
+ * @author <a href="mailto:tv@apache.org">Thomas Vandahl</a>
+ */
+@XmlRootElement(name="url-mapping")
+@XmlAccessorType(XmlAccessType.NONE)
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class URLMappingContainer
+{
+
+    /**
+     * Name of this map.
+     */
+    @XmlAttribute
+    private String name;
+
+    /**
+     * The list of map entries
+     */
+    @JsonProperty("maps")
+    private List<URLMapEntry> urlMapEntries = new CopyOnWriteArrayList<>();
+
+    /**
+     * Set the name of this map.
+     *
+     * @param name
+     *            Name of this map.
+     */
+    protected void setName(String name)
+    {
+        this.name = name;
+    }
+
+    /**
+     * Get the name of this map.
+     *
+     * @return String Name of this map.
+     */
+    public String getName()
+    {
+        return name;
+    }
+
+    /**
+     * Get the list of map entries
+     */
+    @XmlElementWrapper(name="maps")
+    @XmlElement(name="map")
+    public List<URLMapEntry> getMapEntries()
+    {
+        return urlMapEntries;
+    }
+
+    /**
+     * Set new map entries during deserialization
+     *
+     * @param newURLMapEntries the newURLMapEntries to set
+     */
+    protected void setMapEntries(List<URLMapEntry> newURLMapEntries)
+    {
+        this.urlMapEntries = new CopyOnWriteArrayList<URLMapEntry>(newURLMapEntries);
+    }
+}
diff --git a/src/java/org/apache/turbine/services/urlmapper/model/XmlParameterAdapter.java b/src/java/org/apache/turbine/services/urlmapper/model/XmlParameterAdapter.java
new file mode 100644
index 0000000..609905d
--- /dev/null
+++ b/src/java/org/apache/turbine/services/urlmapper/model/XmlParameterAdapter.java
@@ -0,0 +1,62 @@
+package org.apache.turbine.services.urlmapper.model;
+
+/*
+ * 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 java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import javax.xml.bind.annotation.adapters.XmlAdapter;
+
+import org.apache.turbine.services.urlmapper.model.XmlParameterList.XmlParameter;
+
+/**
+ * Creates Map objects from XmlParameterList objects and vice-versa.
+ *
+ * @author <a href="mailto:tv@apache.org">Thomas Vandahl</a>
+ */
+public class XmlParameterAdapter extends XmlAdapter<XmlParameterList, Map<String, String>>
+{
+    /**
+     * @see javax.xml.bind.annotation.adapters.XmlAdapter#unmarshal(java.lang.Object)
+     */
+    @Override
+    public Map<String, String> unmarshal(XmlParameterList xmlList) throws Exception
+    {
+        // Make sure that order is kept
+        return xmlList.getXmlParameters().stream()
+                .collect(Collectors.toMap(xml -> xml.key, xml -> xml.value,
+                        (e1, e2) -> e1, LinkedHashMap::new));
+    }
+
+    /**
+     * @see javax.xml.bind.annotation.adapters.XmlAdapter#marshal(java.lang.Object)
+     */
+    @Override
+    public XmlParameterList marshal(Map<String, String> map) throws Exception
+    {
+        XmlParameterList xmlList = new XmlParameterList();
+        xmlList.setXmlParameters(map.entrySet().stream()
+                .map(entry -> new XmlParameter(entry.getKey(), entry.getValue()))
+                .collect(Collectors.toList()));
+
+        return xmlList;
+    }
+}
diff --git a/src/java/org/apache/turbine/services/urlmapper/model/XmlParameterList.java b/src/java/org/apache/turbine/services/urlmapper/model/XmlParameterList.java
new file mode 100644
index 0000000..7f8a769
--- /dev/null
+++ b/src/java/org/apache/turbine/services/urlmapper/model/XmlParameterList.java
@@ -0,0 +1,108 @@
+package org.apache.turbine.services.urlmapper.model;
+
+/*
+ * 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 java.util.List;
+
+/*
+ * 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 javax.xml.bind.annotation.XmlAccessType;
+import javax.xml.bind.annotation.XmlAccessorType;
+import javax.xml.bind.annotation.XmlAttribute;
+import javax.xml.bind.annotation.XmlElement;
+import javax.xml.bind.annotation.XmlValue;
+
+/**
+ * A JAXB Class for holding a list of entries with key (in an attribute) and a value.
+ *
+ * @author <a href="mailto:tv@apache.org">Thomas Vandahl</a>
+ */
+@XmlAccessorType(XmlAccessType.NONE)
+public class XmlParameterList
+{
+    public static class XmlParameter
+    {
+        @XmlAttribute
+        public String key;
+
+        @XmlValue
+        public String value;
+
+        /**
+         * Default Constructor
+         */
+        public XmlParameter()
+        {
+            // empty
+        }
+
+        /**
+         * Constructor
+         *
+         * @param key the key
+         * @param value the value
+         */
+        public XmlParameter(String key, String value)
+        {
+            this.key = key;
+            this.value = value;
+        }
+    }
+
+    private List<XmlParameter> xmlParameters;
+
+    /**
+     * Get the list of XmlParameters
+     *
+     * @return the xmlParameters
+     */
+    @XmlElement(name="parameter")
+    public List<XmlParameter> getXmlParameters()
+    {
+        return xmlParameters;
+    }
+
+    /**
+     * Set a list of XmlParameters
+     *
+     * @param xmlParameters the xmlParameters to set
+     */
+    public void setXmlParameters(List<XmlParameter> xmlParameters)
+    {
+        this.xmlParameters = xmlParameters;
+    }
+}
diff --git a/src/java/org/apache/turbine/services/urlmapper/model/XmlPatternAdapter.java b/src/java/org/apache/turbine/services/urlmapper/model/XmlPatternAdapter.java
new file mode 100644
index 0000000..68a52c5
--- /dev/null
+++ b/src/java/org/apache/turbine/services/urlmapper/model/XmlPatternAdapter.java
@@ -0,0 +1,50 @@
+package org.apache.turbine.services.urlmapper.model;
+
+/*
+ * 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 java.util.regex.Pattern;
+
+import javax.xml.bind.annotation.adapters.XmlAdapter;
+
+/**
+ * Creates Regex Pattern objects.
+ *
+ * @author <a href="mailto:tv@apache.org">Thomas Vandahl</a>
+ */
+public class XmlPatternAdapter extends XmlAdapter<String, Pattern>
+{
+    /**
+     * @see javax.xml.bind.annotation.adapters.XmlAdapter#unmarshal(java.lang.Object)
+     */
+    @Override
+    public Pattern unmarshal(String urlPattern) throws Exception
+    {
+        return Pattern.compile(urlPattern);
+    }
+
+    /**
+     * @see javax.xml.bind.annotation.adapters.XmlAdapter#marshal(java.lang.Object)
+     */
+    @Override
+    public String marshal(Pattern pattern) throws Exception
+    {
+        return pattern.pattern();
+    }
+}
diff --git a/src/java/org/apache/turbine/services/urlmapper/package.html b/src/java/org/apache/turbine/services/urlmapper/package.html
new file mode 100644
index 0000000..2f8516f
--- /dev/null
+++ b/src/java/org/apache/turbine/services/urlmapper/package.html
@@ -0,0 +1,29 @@
+<!--
+ 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.
+-->
+<html>
+<head>
+<!-- head part is ignored -->
+</head>
+
+<body>
+Provide back-and-forth-mapping facilities for simplified URLs
+<br>
+<font size="-2">$Id$</font>
+</body>
+</html>
diff --git a/src/test/org/apache/turbine/services/urlmapper/TurbineURLMapperServiceTest.java b/src/test/org/apache/turbine/services/urlmapper/TurbineURLMapperServiceTest.java
new file mode 100644
index 0000000..aaaa62e
--- /dev/null
+++ b/src/test/org/apache/turbine/services/urlmapper/TurbineURLMapperServiceTest.java
@@ -0,0 +1,353 @@
+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.junit.jupiter.api.Assertions.fail;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Spliterator;
+import java.util.SplittableRandom;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.function.IntConsumer;
+import java.util.stream.IntStream;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.fulcrum.parser.ParameterParser;
+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.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+public class TurbineURLMapperServiceTest extends BaseTestCase
+{
+    private TurbineConfig tc = null;
+
+    private URLMapperService urlMapper = null;
+
+    @BeforeEach
+    public void setUp() throws Exception
+    {
+        tc =
+                new TurbineConfig(
+                        ".",
+                        "/conf/test/TurbineURLMapperServiceTest.properties");
+        tc.initialize();
+
+        urlMapper = (URLMapperService) TurbineServices.getInstance().getService(URLMapperService.SERVICE_NAME);
+    }
+
+    @AfterEach
+    public void tearDown() throws Exception
+    {
+        if (tc != null)
+        {
+            tc.dispose();
+        }
+    }
+
+    /**
+     * Tests
+     *
+     * <code>scheme://bob/wow/damn2/bookId/123</code>
+     * <code>scheme://bob/wow/book/123</code>
+     * <p>
+     * and
+     *
+     * <code>scheme://bob/wow/damn2/bookId/123/template/Book.vm?detail=1&detail=2&view=collapsed</code>
+     * <code>scheme://bob/wow/book/123/1?view=collapsed</code>
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testMapToURL() throws Exception
+    {
+        assertNotNull(urlMapper);
+        HttpServletRequest request = getMockRequest();
+        HttpServletResponse response = Mockito.mock(HttpServletResponse.class);
+
+        PipelineData pipelineData = getPipelineData(request, response, tc.getTurbine().getServletConfig());
+        assertNotNull(pipelineData);
+
+        TemplateURI uri = new TemplateURI(pipelineData.getRunData());
+        uri.clearResponse(); // avoid encoding on mocked HTTPServletResponse
+        uri.addPathInfo("bookId", 123);
+        uri.setTemplate("Book.vm");
+        uri.addQueryData("detail", 0);
+
+        urlMapper.mapToURL(uri);
+        assertEquals("/wow/book/123", uri.getRelativeLink());
+        assertTrue(uri.getPathInfo().isEmpty());
+        assertTrue(uri.getQueryData().isEmpty());
+
+        uri = new TemplateURI(pipelineData.getRunData());
+        uri.clearResponse(); // avoid encoding on mocked HTTPServletResponse
+        uri.addPathInfo("bookId", 123);
+        uri.setTemplate("Book.vm");
+        uri.addQueryData("detail", 1);
+        uri.addQueryData("detail", 2);
+        uri.addQueryData("view", "collapsed");
+
+        urlMapper.mapToURL(uri);
+        assertEquals("/wow/book/123/1?view=collapsed", uri.getRelativeLink());
+        assertTrue(uri.getPathInfo().isEmpty());
+        assertEquals(1, uri.getQueryData().size());
+    }
+
+    /**
+     * Tests
+     *
+     * <code>scheme:///app/book/123/4</code>
+     * <code>scheme:///wow/damn2/detail/4/bookId/123</code>
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testMapFromURL() throws Exception
+    {
+        assertNotNull(urlMapper);
+        HttpServletRequest request = getMockRequest();
+        HttpServletResponse response = Mockito.mock(HttpServletResponse.class);
+
+        PipelineData pipelineData = getPipelineData(request, response, tc.getTurbine().getServletConfig());
+        assertNotNull(pipelineData);
+        ParameterParser pp = pipelineData.get(Turbine.class, ParameterParser.class);
+        assertNotNull(pp);
+        assertTrue(pp.keySet().isEmpty());
+
+        urlMapper.mapFromURL("/app/book/123/4", pp);
+
+        assertEquals(3, pp.keySet().size());
+        assertEquals(123, pp.getInt("bookId"));
+        assertEquals("Book.vm", pp.getString("template"));
+        assertEquals(4, pp.getInt("detail"));
+
+        // double check
+        TemplateURI uri = new TemplateURI(pipelineData.getRunData());
+        uri.clearResponse();
+        uri.addPathInfo(pp);
+        assertEquals("/wow/damn2/detail/4/bookId/123", uri.getRelativeLink());
+        urlMapper.mapToURL(uri);
+        assertEquals("/wow/book/123/4", uri.getRelativeLink());
+    }
+
+
+    @Tag("performance")
+    @Test
+    public void testPerformance() throws Exception
+    {
+        assertNotNull(urlMapper);
+        int templateURIs = 5;
+        List<AtomicLong> counterSum = new ArrayList<>();
+        List<AtomicInteger> counters = new ArrayList<>();
+        for (int i = 0; i < templateURIs; i++)
+        {
+            counters.add(i, new AtomicInteger(0));
+            counterSum.add(i, new AtomicLong(0L));
+        }
+        int calls = 10_000; // above 1024, set max total of parser pool2 in fulcrum component configuration   ..
+        boolean parallel = false;
+        IntStream range = IntStream.range(0, calls);
+        if (parallel)
+        {
+            range = range.parallel();
+        }
+
+        SplittableRandom sr = new SplittableRandom();
+
+//        range
+//        .peek(e -> System.out.println("current value: " + e))
+//        .forEach( actionInt -> {
+//        	runCheck(templateURIs, counterSum, counters, parallel, sr);
+//        });
+
+        Spliterator.OfInt spliterator1 = range.spliterator();
+        Spliterator.OfInt spliterator2 = spliterator1.trySplit();
+
+        System.out.println("s1 estimateSize: " + spliterator1.estimateSize());
+        spliterator1.forEachRemaining((IntConsumer) i ->
+        {
+            runCheck(templateURIs, counterSum, counters, parallel, sr);
+        });
+        System.out.println("s2 estimateSize: " + spliterator2.estimateSize());
+        spliterator2.forEachRemaining((IntConsumer) i ->
+        {
+            runCheck(templateURIs, counterSum, counters, parallel, sr);
+        });
+
+        for (int i = 0; i < counters.size() - 1; i++)
+        {
+            long time = counterSum.get(i).longValue() / 1_000_000;
+            int count = counters.get(i).get();
+            TemplateURI turi = getURI(i);
+            String relativeLink = turi.getRelativeLink();
+            callMapToUrl(turi);
+            System.out.printf("time = %dms (%d calls),average time = %5.3fmics, uri=%s, map=%s%n", time, count,
+                    count > 0 ? ((double) time * 1000 / count) : 0,
+                    relativeLink, turi.getRelativeLink());
+        }
+        System.out.printf("total time = %dms (%d total calls) parallel:%s%n",
+                counterSum.stream().mapToInt(i -> i.intValue()).sum() / 1_000_000,
+                counters.stream().mapToInt(i -> i.intValue()).sum(),
+                parallel
+        );
+    }
+
+    private void runCheck(int templateURIs, List<AtomicLong> counterSum, List<AtomicInteger> counters, boolean parallel,
+                          SplittableRandom sr)
+    {
+        int randomNum = sr.nextInt(templateURIs);
+        TemplateURI turi = getURI(randomNum);
+        long time = System.nanoTime();
+        try
+        {
+            callMapToUrl(turi);
+        }
+        finally
+        {
+            time = System.nanoTime() - time;
+            counterSum.get(randomNum).addAndGet(time);
+            counters.get(randomNum).incrementAndGet();
+        }
+    }
+
+    /**
+     * to get a fresh URI
+     *
+     * @param tnr
+     * @return
+     */
+    private TemplateURI getURI(int tnr)
+    {
+        TemplateURI turi = null;
+        switch (tnr)
+        {
+            case 0:
+                turi = getURI1();
+                break;
+            case 1:
+                turi = getURI2();
+                break;
+            case 2:
+                turi = getURI3();
+                break;
+            case 3:
+                turi = getURI4();
+                break;
+            case 4:
+                turi = getURI5();
+                break;
+            default:
+                break;
+        }
+        return turi;
+    }
+
+    private TemplateURI getURI1()
+    {
+        TemplateURI uri = new TemplateURI(getRunData());
+        uri.clearResponse(); // avoid encoding on mocked HTTPServletResponse
+        uri.addPathInfo("bookId", 123);
+        uri.setTemplate("Book.vm");
+        uri.addQueryData("detail", 0);
+        return uri;
+    }
+
+    private TemplateURI getURI2()
+    {
+        TemplateURI uri2 = new TemplateURI(getRunData());
+        uri2.clearResponse();
+        uri2.addPathInfo("bookId", 123);
+        uri2.setTemplate("Book.vm");
+        uri2.addQueryData("detail", 1);
+        uri2.addQueryData("detail", 2);
+        uri2.addQueryData("view", "collapsed");
+        return uri2;
+    }
+
+    private TemplateURI getURI3()
+    {
+        TemplateURI uri3 = new TemplateURI(getRunData());
+        uri3.clearResponse();
+        uri3.addPathInfo("id", 1234);
+        uri3.addPathInfo("role", "guest");
+        uri3.addPathInfo("language", "de");
+        return uri3;
+    }
+
+    private TemplateURI getURI4()
+    {
+        TemplateURI uri4 = new TemplateURI(getRunData());
+        uri4.clearResponse();
+        uri4.addPathInfo("js_pane", "random-id-123-abc");
+        uri4.addPathInfo("role", "anon");
+        uri4.addPathInfo("media-type", "html");
+        uri4.setTemplate("Registerone.vm");
+        return uri4;
+    }
+
+    private TemplateURI getURI5()
+    {
+        TemplateURI uri5 = new TemplateURI(getRunData());
+        uri5.clearResponse();
+        uri5.addPathInfo("js_pane", "another-random-id-876-dfg");
+        uri5.addPathInfo("role", "anon");
+        uri5.addPathInfo("media-type", "html");
+        uri5.addPathInfo("page", "Contact");
+        return uri5;
+    }
+
+    private RunData getRunData()
+    {
+        HttpServletRequest request = getMockRequest();
+        HttpServletResponse response = Mockito.mock(HttpServletResponse.class);
+        try
+        {
+            PipelineData pipelineData = getPipelineData(request, response, tc.getTurbine().getServletConfig());
+            assertNotNull(pipelineData);
+            return pipelineData.getRunData();
+        }
+        catch (Exception e)
+        {
+            fail();
+        }
+        return null;
+    }
+
+    private void callMapToUrl(TemplateURI uri)
+    {
+        urlMapper.mapToURL(uri);
+        assertTrue(uri.getPathInfo().isEmpty(), "path is not empty:" + uri.getPathInfo());
+    }
+}
diff --git a/src/test/org/apache/turbine/services/urlmapper/TurbineYamlURLMapperServiceTest.java b/src/test/org/apache/turbine/services/urlmapper/TurbineYamlURLMapperServiceTest.java
new file mode 100644
index 0000000..b5bdc7a
--- /dev/null
+++ b/src/test/org/apache/turbine/services/urlmapper/TurbineYamlURLMapperServiceTest.java
@@ -0,0 +1,206 @@
+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/URLMappingContainerTest.java b/src/test/org/apache/turbine/services/urlmapper/model/URLMappingContainerTest.java
new file mode 100644
index 0000000..45b6412
--- /dev/null
+++ b/src/test/org/apache/turbine/services/urlmapper/model/URLMappingContainerTest.java
@@ -0,0 +1,65 @@
+package org.apache.turbine.services.urlmapper.model;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import java.io.FileInputStream;
+import java.io.InputStream;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Pattern;
+
+import javax.xml.bind.JAXBContext;
+import javax.xml.bind.Unmarshaller;
+
+import org.junit.Before;
+import org.junit.Test;
+
+public class URLMappingContainerTest
+{
+    URLMappingContainer container;
+
+    @Before
+    public void setUp() throws Exception
+    {
+        try (InputStream reader = new FileInputStream("conf/turbine-url-mapping.xml"))
+        {
+            JAXBContext jaxb = JAXBContext.newInstance(URLMappingContainer.class);
+            Unmarshaller unmarshaller = jaxb.createUnmarshaller();
+            container = (URLMappingContainer) unmarshaller.unmarshal(reader);
+        }
+    }
+
+    @Test
+    public void testGetName()
+    {
+        assertNotNull(container);
+        assertEquals("default", container.getName());
+    }
+
+    @Test
+    public void testGetMapEntries()
+    {
+        assertNotNull(container);
+
+        List<URLMapEntry> mapEntries = container.getMapEntries();
+        assertNotNull(mapEntries);
+        assertNotEquals(0, mapEntries.size());
+
+        URLMapEntry entry = mapEntries.get(0);
+        assertNotNull(entry);
+
+        Pattern pattern = entry.getUrlPattern();
+        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"));
+    }
+
+}
diff --git a/src/test/org/apache/turbine/services/urlmapper/model/YamlURLMappingContainerTest.java b/src/test/org/apache/turbine/services/urlmapper/model/YamlURLMappingContainerTest.java
new file mode 100644
index 0000000..b35f310
--- /dev/null
+++ b/src/test/org/apache/turbine/services/urlmapper/model/YamlURLMappingContainerTest.java
@@ -0,0 +1,66 @@
+package org.apache.turbine.services.urlmapper.model;
+
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.io.FileInputStream;
+import java.io.InputStream;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Pattern;
+
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
+
+@Tag("yaml")
+public class YamlURLMappingContainerTest
+{
+    private static URLMappingContainer container;
+
+    @BeforeAll
+    public static void setUp() throws Exception
+    {
+        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);
+        }
+    }
+
+    @Test
+    public void testGetName()
+    {
+        assertNotNull(container);
+        assertEquals("default", container.getName());
+    }
+
+    @Test
+    public void testGetMapEntries()
+    {
+        assertNotNull(container);
+
+        List<URLMapEntry> mapEntries = container.getMapEntries();
+        assertNotNull(mapEntries);
+        assertNotEquals(0, mapEntries.size());
+
+        URLMapEntry entry = mapEntries.get(0);
+        assertNotNull(entry);
+
+        Pattern pattern = entry.getUrlPattern();
+        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"));
+    }
+
+}
diff --git a/src/test/org/apache/turbine/test/BaseTestCase.java b/src/test/org/apache/turbine/test/BaseTestCase.java
index 9d539ea..dc1ff2f 100644
--- a/src/test/org/apache/turbine/test/BaseTestCase.java
+++ b/src/test/org/apache/turbine/test/BaseTestCase.java
@@ -41,10 +41,10 @@
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import javax.servlet.http.HttpSession;
-import javax.xml.parsers.FactoryConfigurationError;
 
 import org.apache.log4j.PropertyConfigurator;
-import org.apache.log4j.xml.DOMConfigurator;
+import org.apache.logging.log4j.core.config.ConfigurationSource;
+import org.apache.logging.log4j.core.config.Configurator;
 import org.apache.turbine.TurbineConstants;
 import org.apache.turbine.pipeline.PipelineData;
 import org.apache.turbine.services.TurbineServices;
@@ -65,43 +65,13 @@
  */
 public abstract class BaseTestCase
 {
-    static File log4jFile = new File("conf/test/log4j.xml");
+    static File log4j2File = new File("conf/test/log4j2.xml");
 
     @BeforeClass
     public static void baseInit()
             throws Exception
     {
-
-        if (log4jFile.getName().endsWith(".xml"))
-        {
-            // load XML type configuration
-            // NOTE: Only system property expansion available
-            try
-            {
-                DOMConfigurator.configure(log4jFile.getAbsolutePath());
-            }
-            catch (FactoryConfigurationError e)
-            {
-                System.err.println("Could not configure Log4J from configuration file "
-                        + log4jFile + ": ");
-                e.printStackTrace();
-            }
-        }
-        else {
-            Properties p = new Properties();
-            try
-            {
-                p.load(new FileInputStream(log4jFile));
-                p.setProperty(TurbineConstants.APPLICATION_ROOT_KEY, new File(".").getAbsolutePath());
-                PropertyConfigurator.configure(p);
-    
-            }
-            catch (FileNotFoundException fnf)
-            {
-                System.err.println("Could not open Log4J configuration file "
-                        + log4jFile);
-            }
-        }
+    	// auto load log4j2 file
     }
 
     protected RunData getRunData(HttpServletRequest request,HttpServletResponse response,ServletConfig config) throws Exception {