Keeps in sync with OFBiz trunk HEAD

git-svn-id: https://svn.apache.org/repos/asf/ofbiz/branches/OFBIZ-5312-ofbiz-ecommerce-seo-2013-10-23@1649482 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/applications/product/build.xml b/applications/product/build.xml
index da199ea..03a8430 100644
--- a/applications/product/build.xml
+++ b/applications/product/build.xml
@@ -44,10 +44,12 @@
         <fileset dir="../../framework/webapp/lib" includes="*.jar"/>
         <fileset dir="../../framework/webapp/build/lib" includes="*.jar"/>
         <fileset dir="../../framework/common/build/lib" includes="*.jar"/>
+        <fileset dir="../../framework/base/lib/scripting" includes="*.jar"/>
         <fileset dir="../content/lib" includes="*.jar"/>
         <fileset dir="../content/build/lib" includes="*.jar"/>
         <fileset dir="../party/build/lib" includes="*.jar"/>
         <fileset dir="lib" includes="*.jar"/>
+        <fileset dir="../../framework/catalina/lib" includes="*.jar"/>
     </path>
 
     <target name="init">
diff --git a/applications/product/config/SeoConfig.xml b/applications/product/config/SeoConfig.xml
new file mode 100644
index 0000000..c670ebb
--- /dev/null
+++ b/applications/product/config/SeoConfig.xml
@@ -0,0 +1,116 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<seo-config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../dtd/SeoConfig.xsd">
+    <regexpifmatch>^.*/.*$</regexpifmatch>
+
+    <category-url>
+        <value>enable</value>
+        <!-- enable/disable
+          1. if enable, the product seo url will be /product-name-product-id.html or /category-name-product-name-product-id.html
+          2. if disable, the CatalogUrlServlet.makeCatalogUrl will be used -->
+        <allowed-context-paths>/:/ecommerce</allowed-context-paths>
+        <!-- if category-url is enabled, only context paths listed here will be allowed to use the new seo transformers -->
+        <category-name>disable</category-name>
+        <!-- enable/disable
+          1. if enable, the product seo url will be /category-name-product-name-product-id.html
+          2. if disable, the product seo url will be /product-name-product-id.html -->
+        <category-url-suffix>.html</category-url-suffix>
+        <!-- suffix to add to the product seo url -->
+    </category-url>
+
+    <jsessionid>
+        <anonymous>
+            <value>disable</value>
+            <!-- enable/disable
+              1. when disable, the seo url patterns that contains jsessionid will be applied;
+              2. when enable, the seo url patterns that contains jsessionid will be skipped -->
+        </anonymous>
+        <user>
+            <value>disable</value>
+            <!-- enable/disable
+              1. when disable, the url-patterns under exceptions will be used to skip the seo url patterns that contains jsessionid;
+              2. when enable, the seo url patterns that contains jsessionid will be skipped -->
+            <exceptions>
+                <url-pattern>^.*/(keywordsearch|logout).*$</url-pattern>
+                <!-- sample: ^.*/(keywordsearch|logout).*$ -->
+            </exceptions>
+        </user>
+    </jsessionid>
+
+    <url-configs>
+        <url-config>
+            <description>sample: remove jsessionid</description>
+            <url-pattern>^(.*);jsessionid=(.*)jvm[1-9](.*)$</url-pattern>
+            <seo>
+                <replacement>$1$3</replacement>
+            </seo>
+            <forward>
+                <replacement>$1$3</replacement>
+                <responsecode>301</responsecode>
+            </forward>
+        </url-config>
+
+        <url-config>
+            <description>sample: remove /ecommerce/main</description>
+            <url-pattern>^/ecommerce/main$</url-pattern>
+            <seo>
+                <replacement>/ecommerce/</replacement>
+            </seo>
+            <forward>
+                <replacement>/ecommerce/</replacement>
+                <responsecode>301</responsecode>
+            </forward>
+        </url-config>
+
+        <url-config>
+            <description>sample: remove /main</description>
+            <url-pattern>^/main$</url-pattern>
+            <seo>
+                <replacement>/</replacement>
+            </seo>
+            <forward>
+                <replacement>/</replacement>
+                <responsecode>301</responsecode>
+            </forward>
+        </url-config>
+    </url-configs>
+
+    <char-filters>
+        <char-filter>
+            <character-pattern>\u00fc</character-pattern>
+            <replacement>ue</replacement>
+        </char-filter>
+        <char-filter>
+            <character-pattern>\u00e4</character-pattern>
+            <replacement>ae</replacement>
+        </char-filter>
+        <char-filter>
+            <character-pattern>\u00f6</character-pattern>
+            <replacement>oe</replacement>
+        </char-filter>
+        <char-filter>
+            <character-pattern>\u00df</character-pattern>
+            <replacement>ss</replacement>
+        </char-filter>
+        <char-filter>
+            <character-pattern>\\+</character-pattern>
+            <replacement>und</replacement>
+        </char-filter>
+        <char-filter>
+            <character-pattern>\u0026</character-pattern>
+            <replacement>und</replacement>
+        </char-filter>
+        <char-filter>
+            <character-pattern>è</character-pattern>
+            <replacement>e</replacement>
+        </char-filter>
+        <!-- please keep the following 2 filters, don't remove them -->
+        <char-filter>
+            <character-pattern>[^A-Za-z0-9+-]</character-pattern>
+            <replacement>-</replacement>
+        </char-filter>
+        <char-filter>
+            <character-pattern>-{2,}</character-pattern>
+            <replacement>-</replacement>
+        </char-filter>
+    </char-filters>
+</seo-config>
diff --git a/applications/product/config/freemarkerTransforms.properties b/applications/product/config/freemarkerTransforms.properties
index 98d314c..450c52d 100644
--- a/applications/product/config/freemarkerTransforms.properties
+++ b/applications/product/config/freemarkerTransforms.properties
@@ -21,6 +21,8 @@
 
 # entries are in the form: key=transform name, property=transform class name
 
-ofbizCatalogAltUrl=org.ofbiz.product.category.OfbizCatalogAltUrlTransform
-ofbizCatalogUrl=org.ofbiz.product.category.CatalogUrlDirective
+#ofbizCatalogAltUrl=org.ofbiz.product.category.ftl.OfbizCatalogAltUrlTransform
+#ofbizCatalogUrl=org.ofbiz.product.category.ftl.CatalogUrlDirective
+ofbizCatalogAltUrl=org.ofbiz.product.category.ftl.CatalogAltUrlSeoTransform
+ofbizCatalogUrl=org.ofbiz.product.category.ftl.CatalogUrlSeoTransform
 
diff --git a/applications/product/dtd/SeoConfig.xsd b/applications/product/dtd/SeoConfig.xsd
new file mode 100644
index 0000000..194e830
--- /dev/null
+++ b/applications/product/dtd/SeoConfig.xsd
@@ -0,0 +1,112 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified">
+    <xs:element name="seo-config">
+        <xs:complexType>
+            <xs:sequence>
+                <xs:element minOccurs="0" maxOccurs="1" ref="regexpifmatch"/>
+                <xs:element minOccurs="0" maxOccurs="1" ref="category-url"/>
+                <xs:element minOccurs="0" maxOccurs="1" ref="jsessionid"/>
+                <xs:element minOccurs="0" maxOccurs="1" ref="url-configs"/>
+                <xs:element minOccurs="0" maxOccurs="1" ref="char-filters"/>
+            </xs:sequence>
+        </xs:complexType>
+    </xs:element>
+    <xs:element name="regexpifmatch"/>
+    <xs:element name="category-url">
+        <xs:complexType>
+            <xs:sequence>
+                <xs:element minOccurs="1" maxOccurs="1" ref="value"/>
+                <xs:element minOccurs="0" maxOccurs="1" ref="allowed-context-paths"/>
+                <xs:element minOccurs="0" maxOccurs="1" ref="category-name"/>
+                <xs:element minOccurs="0" maxOccurs="1" ref="category-url-suffix"/>
+            </xs:sequence>
+        </xs:complexType>
+    </xs:element>
+    <xs:element name="value" default="disable"/>
+    <xs:element name="allowed-context-paths"/>
+    <xs:element name="category-name" default="disable"/>
+    <xs:element name="category-url-suffix" default=".html"/>
+    <xs:element name="jsessionid">
+        <xs:complexType>
+            <xs:sequence>
+                <xs:element minOccurs="0" maxOccurs="1" ref="anonymous"/>
+                <xs:element minOccurs="0" maxOccurs="1" ref="user"/>
+            </xs:sequence>
+        </xs:complexType>
+    </xs:element>
+    <xs:element name="anonymous">
+        <xs:complexType>
+            <xs:sequence>
+                <xs:element minOccurs="0" maxOccurs="1" ref="value"/>
+            </xs:sequence>
+        </xs:complexType>
+    </xs:element>
+    <xs:element name="user">
+        <xs:complexType>
+            <xs:sequence>
+                <xs:element minOccurs="0" maxOccurs="1" ref="value"/>
+                <xs:element minOccurs="0" maxOccurs="1" ref="exceptions"/>
+            </xs:sequence>
+        </xs:complexType>
+    </xs:element>
+    <xs:element name="exceptions">
+        <xs:complexType>
+            <xs:sequence>
+                <xs:element minOccurs="0" maxOccurs="unbounded" ref="url-pattern"/>
+            </xs:sequence>
+        </xs:complexType>
+    </xs:element>
+    <xs:element name="url-configs">
+        <xs:complexType>
+            <xs:sequence>
+                <xs:element minOccurs="0" maxOccurs="unbounded" ref="url-config"/>
+            </xs:sequence>
+        </xs:complexType>
+    </xs:element>
+    <xs:element name="char-filters">
+        <xs:complexType>
+            <xs:sequence>
+                <xs:element minOccurs="0" maxOccurs="unbounded" ref="char-filter"/>
+            </xs:sequence>
+        </xs:complexType>
+    </xs:element>
+    <xs:element name="url-config">
+        <xs:complexType>
+            <xs:sequence>
+                <xs:element minOccurs="0" maxOccurs="1" ref="description"/>
+                <xs:element minOccurs="1" maxOccurs="1" ref="url-pattern"/>
+                <xs:element minOccurs="1" maxOccurs="1" ref="seo"/>
+                <xs:element minOccurs="0" maxOccurs="1" ref="forward"/>
+            </xs:sequence>
+        </xs:complexType>
+    </xs:element>
+    <xs:element name="description"/>
+    <xs:element name="url-pattern"/>
+    <xs:element name="seo">
+        <xs:complexType>
+            <xs:sequence>
+                <xs:element minOccurs="1" maxOccurs="1" ref="replacement"/>
+            </xs:sequence>
+        </xs:complexType>
+    </xs:element>
+    <xs:element name="forward">
+        <xs:complexType>
+            <xs:sequence>
+                <xs:element minOccurs="1" maxOccurs="1" ref="replacement"/>
+                <xs:element minOccurs="0" maxOccurs="1" ref="responsecode"/>
+            </xs:sequence>
+        </xs:complexType>
+    </xs:element>
+    <xs:element name="replacement"/>
+    <xs:element name="responsecode" default="301"/>
+    <xs:element name="char-filter">
+        <xs:complexType>
+            <xs:sequence>
+                <xs:element minOccurs="1" maxOccurs="1" ref="character-pattern"/>
+                <xs:element minOccurs="1" maxOccurs="1" ref="replacement"/>
+            </xs:sequence>
+        </xs:complexType>
+    </xs:element>
+    <xs:element name="character-pattern"/>
+</xs:schema>
diff --git a/applications/product/src/org/ofbiz/product/category/CatalogUrlSeoFilter.java b/applications/product/src/org/ofbiz/product/category/CatalogUrlSeoFilter.java
new file mode 100644
index 0000000..a292c55
--- /dev/null
+++ b/applications/product/src/org/ofbiz/product/category/CatalogUrlSeoFilter.java
@@ -0,0 +1,71 @@
+/*******************************************************************************
+ * 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.
+ *******************************************************************************/
+package org.ofbiz.product.category;
+
+import java.io.IOException;
+
+import javax.servlet.FilterChain;
+import javax.servlet.ServletContext;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.ofbiz.base.util.UtilValidate;
+import org.ofbiz.common.UrlServletHelper;
+import org.ofbiz.entity.Delegator;
+import org.ofbiz.product.category.ftl.CatalogUrlSeoTransform;
+
+public class CatalogUrlSeoFilter extends CatalogUrlFilter {
+
+    public final static String module = CatalogUrlSeoFilter.class.getName();
+
+    protected static String defaultLocaleString = null;
+    protected static String redirectUrl = null;
+
+    /**
+     * @see javax.servlet.Filter#doFilter(javax.servlet.ServletRequest, javax.servlet.ServletResponse, javax.servlet.FilterChain)
+     */
+    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
+        HttpServletRequest httpRequest = (HttpServletRequest) request;
+        HttpServletResponse httpResponse = (HttpServletResponse) response;
+        Delegator delegator = (Delegator) httpRequest.getSession().getServletContext().getAttribute("delegator");
+
+        // Get ServletContext
+        ServletContext servletContext = config.getServletContext();
+
+        // Set request attribute and session
+        UrlServletHelper.setRequestAttributes(request, delegator, servletContext);
+
+        // set initial parameters
+        String initDefaultLocalesString = config.getInitParameter("defaultLocaleString");
+        String initRedirectUrl = config.getInitParameter("redirectUrl");
+        defaultLocaleString = UtilValidate.isNotEmpty(initDefaultLocalesString) ? initDefaultLocalesString : "";
+        redirectUrl = UtilValidate.isNotEmpty(initRedirectUrl) ? initRedirectUrl : "";
+
+        // set the ServletContext in the request for future use
+        httpRequest.setAttribute("servletContext", config.getServletContext());
+        if (CatalogUrlSeoTransform.forwardUri(httpRequest, httpResponse, delegator, ControlServlet.controlServlet)) {
+            return;
+        }
+        super.doFilter(httpRequest, httpResponse, chain);
+    }
+
+}
diff --git a/applications/product/src/org/ofbiz/product/category/CategoryServices.java b/applications/product/src/org/ofbiz/product/category/CategoryServices.java
index 2a81c99..bceec73 100644
--- a/applications/product/src/org/ofbiz/product/category/CategoryServices.java
+++ b/applications/product/src/org/ofbiz/product/category/CategoryServices.java
@@ -524,7 +524,7 @@
                 }
             }
         } catch (GenericEntityException e) {
-            e.printStackTrace();
+            Debug.logWarning(e, module);
             return "error";
         }
         return "success";
diff --git a/applications/product/src/org/ofbiz/product/category/ControlServlet.java b/applications/product/src/org/ofbiz/product/category/ControlServlet.java
new file mode 100644
index 0000000..efc21c5
--- /dev/null
+++ b/applications/product/src/org/ofbiz/product/category/ControlServlet.java
@@ -0,0 +1,68 @@
+/*******************************************************************************
+ * 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.
+ *******************************************************************************/
+package org.ofbiz.product.category;
+
+import javax.servlet.ServletConfig;
+import javax.servlet.ServletContext;
+import javax.servlet.ServletException;
+
+import org.ofbiz.base.util.UtilValidate;
+
+/**
+ * ControlServlet.java - Master servlet for the web application.
+ */
+@SuppressWarnings("serial")
+public class ControlServlet extends org.ofbiz.webapp.control.ControlServlet {
+
+    public static final String module = ControlServlet.class.getName();
+
+    protected static String defaultPage = null;
+    protected static String pageNotFound = null;
+    protected static String controlServlet = null;
+
+    public ControlServlet() {
+        super();
+    }
+
+    /**
+     * @see javax.servlet.Servlet#init(javax.servlet.ServletConfig)
+     */
+    public void init(ServletConfig config) throws ServletException {
+        super.init(config);
+
+        ServletContext context = this.getServletContext();
+        if (UtilValidate.isEmpty(defaultPage)) {
+            defaultPage = context.getInitParameter("defaultPage");
+        }
+        if (UtilValidate.isEmpty(defaultPage)) {
+            defaultPage = "/main";
+        }
+        if (UtilValidate.isEmpty(pageNotFound)) {
+            pageNotFound = context.getInitParameter("pageNotFound");
+        }
+        if (UtilValidate.isEmpty(pageNotFound)) {
+            pageNotFound = "/pagenotfound";
+        }
+
+        if (defaultPage.startsWith("/") && defaultPage.lastIndexOf("/") > 0) {
+            controlServlet = defaultPage.substring(1);
+            controlServlet = controlServlet.substring(0, controlServlet.indexOf("/"));
+        }
+    }
+}
diff --git a/applications/product/src/org/ofbiz/product/category/SeoCatalogUrlServlet.java b/applications/product/src/org/ofbiz/product/category/SeoCatalogUrlServlet.java
new file mode 100644
index 0000000..3768186
--- /dev/null
+++ b/applications/product/src/org/ofbiz/product/category/SeoCatalogUrlServlet.java
@@ -0,0 +1,206 @@
+/*******************************************************************************
+ * 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.
+ *******************************************************************************/
+package org.ofbiz.product.category;
+
+import java.io.IOException;
+import java.util.List;
+
+import javax.servlet.RequestDispatcher;
+import javax.servlet.ServletConfig;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import javolution.util.FastList;
+
+import org.ofbiz.base.util.Debug;
+import org.ofbiz.base.util.StringUtil;
+import org.ofbiz.base.util.UtilMisc;
+import org.ofbiz.base.util.UtilValidate;
+import org.ofbiz.entity.Delegator;
+import org.ofbiz.entity.GenericEntityException;
+
+/**
+ * SeoCatalogUrlServlet.java
+ */
+@SuppressWarnings("serial")
+public class SeoCatalogUrlServlet extends HttpServlet {
+
+    public static final String module = SeoCatalogUrlServlet.class.getName();
+    public static final String CATALOG_URL_MOUNT_POINT = "products";
+    public static final String PRODUCT_REQUEST = "product";
+    public static final String CATEGORY_REQUEST = "category";
+
+    public SeoCatalogUrlServlet() {
+        super();
+    }
+
+    /**
+     * @see javax.servlet.http.HttpServlet#init(javax.servlet.ServletConfig)
+     */
+    @Override
+    public void init(ServletConfig config) throws ServletException {
+        super.init(config);
+    }
+
+    /**
+     * @see javax.servlet.http.HttpServlet#doPost(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
+     */
+    @Override
+    public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
+        doGet(request, response);
+    }
+
+    /**
+     * @see javax.servlet.http.HttpServlet#doGet(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
+     */
+    @Override
+    public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
+        Delegator delegator = (Delegator) getServletContext().getAttribute("delegator");
+
+        String pathInfo = request.getPathInfo();
+        List<String> pathElements = StringUtil.split(pathInfo, "/");
+
+        // look for productId
+        String productId = null;
+        try {
+            String lastPathElement = pathElements.get(pathElements.size() - 1);
+            if (lastPathElement.startsWith("p_") || delegator.findOne("Product", UtilMisc.toMap("productId", lastPathElement), true) != null) {
+                if (lastPathElement.startsWith("p_")) {
+                    productId = lastPathElement.substring(2);
+                } else {
+                    productId = lastPathElement;
+                }
+                pathElements.remove(pathElements.size() - 1);
+            }
+        } catch (GenericEntityException e) {
+            Debug.logError(e, "Error looking up product info for ProductUrl with path info [" + pathInfo + "]: " + e.toString(), module);
+        }
+
+        // get category info going with the IDs that remain
+        String categoryId = null;
+        if (pathElements.size() == 1) {
+            CategoryWorker.setTrail(request, pathElements.get(0), null);
+            categoryId = pathElements.get(0);
+        } else if (pathElements.size() == 2) {
+            CategoryWorker.setTrail(request, pathElements.get(1), pathElements.get(0));
+            categoryId = pathElements.get(1);
+        } else if (pathElements.size() > 2) {
+            List<String> trail = CategoryWorker.getTrail(request);
+            if (trail == null) {
+                trail = FastList.newInstance();
+            }
+
+            if (trail.contains(pathElements.get(0))) {
+                // first category is in the trail, so remove it everything after that and fill it in with the list from the pathInfo
+                int firstElementIndex = trail.indexOf(pathElements.get(0));
+                while (trail.size() > firstElementIndex) {
+                    trail.remove(firstElementIndex);
+                }
+                trail.addAll(pathElements);
+            } else {
+                // first category is NOT in the trail, so clear out the trail and use the pathElements list
+                trail.clear();
+                trail.addAll(pathElements);
+            }
+            CategoryWorker.setTrail(request, trail);
+            categoryId = pathElements.get(pathElements.size() - 1);
+        }
+        if (categoryId != null) {
+            request.setAttribute("productCategoryId", categoryId);
+        }
+
+        String rootCategoryId = null;
+        if (pathElements.size() >= 1) {
+            rootCategoryId = pathElements.get(0);
+        }
+        if (rootCategoryId != null) {
+            request.setAttribute("rootCategoryId", rootCategoryId);
+        }
+
+        if (productId != null) {
+            request.setAttribute("product_id", productId);
+            request.setAttribute("productId", productId);
+        }
+
+        RequestDispatcher rd = request.getRequestDispatcher("/" + (UtilValidate.isEmpty(SeoControlServlet.controlServlet) ? "" : (SeoControlServlet.controlServlet + "/"))
+                + (productId != null ? PRODUCT_REQUEST : CATEGORY_REQUEST));
+        rd.forward(request, response);
+    }
+
+    /**
+     * @see javax.servlet.http.HttpServlet#destroy()
+     */
+    @Override
+    public void destroy() {
+        super.destroy();
+    }
+
+    public static String makeCatalogUrl(HttpServletRequest request, String productId, String currentCategoryId, String previousCategoryId) {
+        StringBuilder urlBuilder = new StringBuilder();
+        urlBuilder.append(request.getSession().getServletContext().getContextPath());
+        if (urlBuilder.charAt(urlBuilder.length() - 1) != '/') {
+            urlBuilder.append("/");
+        }
+        urlBuilder.append(CATALOG_URL_MOUNT_POINT);
+
+        if (UtilValidate.isNotEmpty(currentCategoryId)) {
+            List<String> trail = CategoryWorker.getTrail(request);
+            trail = CategoryWorker.adjustTrail(trail, currentCategoryId, previousCategoryId);
+            for (String trailCategoryId : trail) {
+                if ("TOP".equals(trailCategoryId)) continue;
+                urlBuilder.append("/");
+                urlBuilder.append(trailCategoryId);
+            }
+        }
+
+        if (UtilValidate.isNotEmpty(productId)) {
+            urlBuilder.append("/p_");
+            urlBuilder.append(productId);
+        }
+
+        return urlBuilder.toString();
+    }
+
+    public static String makeCatalogUrl(String contextPath, List<String> crumb, String productId, String currentCategoryId, String previousCategoryId) {
+        StringBuilder urlBuilder = new StringBuilder();
+        urlBuilder.append(contextPath);
+        if (urlBuilder.charAt(urlBuilder.length() - 1) != '/') {
+            urlBuilder.append("/");
+        }
+        urlBuilder.append(CATALOG_URL_MOUNT_POINT);
+
+        if (UtilValidate.isNotEmpty(currentCategoryId)) {
+            crumb = CategoryWorker.adjustTrail(crumb, currentCategoryId, previousCategoryId);
+            for (String trailCategoryId : crumb) {
+                if ("TOP".equals(trailCategoryId)) continue;
+                urlBuilder.append("/");
+                urlBuilder.append(trailCategoryId);
+            }
+        }
+
+        if (UtilValidate.isNotEmpty(productId)) {
+            urlBuilder.append("/p_");
+            urlBuilder.append(productId);
+        }
+
+        return urlBuilder.toString();
+    }
+}
diff --git a/applications/product/src/org/ofbiz/product/category/SeoConfigUtil.java b/applications/product/src/org/ofbiz/product/category/SeoConfigUtil.java
new file mode 100644
index 0000000..fdbc46f
--- /dev/null
+++ b/applications/product/src/org/ofbiz/product/category/SeoConfigUtil.java
@@ -0,0 +1,537 @@
+/*******************************************************************************
+ * 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.
+ *******************************************************************************/
+package org.ofbiz.product.category;
+
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.net.URL;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.servlet.http.HttpServletResponse;
+import javax.xml.parsers.ParserConfigurationException;
+
+import javolution.util.FastList;
+import javolution.util.FastMap;
+import javolution.util.FastSet;
+
+import org.apache.oro.text.regex.MalformedPatternException;
+import org.apache.oro.text.regex.Pattern;
+import org.apache.oro.text.regex.Perl5Compiler;
+import org.ofbiz.base.util.Debug;
+import org.ofbiz.base.util.StringUtil;
+import org.ofbiz.base.util.UtilURL;
+import org.ofbiz.base.util.UtilValidate;
+import org.ofbiz.base.util.UtilXml;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.NodeList;
+import org.xml.sax.SAXException;
+
+/**
+ * SeoConfigUtil - SEO Configuration file utility.
+ * 
+ */
+public class SeoConfigUtil {
+    private static final String module = SeoConfigUtil.class.getName();
+    private static Perl5Compiler perlCompiler = new Perl5Compiler();
+    private static boolean isInitialed = false;
+    private static boolean categoryUrlEnabled = true;
+    private static boolean categoryNameEnabled = false;
+    private static String categoryUrlSuffix = null;
+    public static final String DEFAULT_REGEXP = "^.*/.*$";
+    private static Pattern regexpIfMatch = null;
+    private static boolean useUrlRegexp = false;
+    private static boolean jSessionIdAnonEnabled = false;
+    private static boolean jSessionIdUserEnabled = false;
+    private static Map<String, String> seoReplacements = null;
+    private static Map<String, Pattern> seoPatterns = null;
+    private static Map<String, String> forwardReplacements = null;
+    private static Map<String, Integer> forwardResponseCodes = null;
+    private static Map<String, String> charFilters = null;
+    private static List<Pattern> userExceptionPatterns = null;
+    private static Set<String> allowedContextPaths = null;
+    private static Map<String, String> specialProductIds = null;
+    public static final String ELEMENT_REGEXPIFMATCH = "regexpifmatch";
+    public static final String ELEMENT_URL_CONFIG = "url-config";
+    public static final String ELEMENT_DESCRIPTION = "description";
+    public static final String ELEMENT_FORWARD = "forward";
+    public static final String ELEMENT_SEO = "seo";
+    public static final String ELEMENT_URLPATTERN = "url-pattern";
+    public static final String ELEMENT_REPLACEMENT = "replacement";
+    public static final String ELEMENT_RESPONSECODE = "responsecode";
+    public static final String ELEMENT_JSESSIONID = "jsessionid";
+    public static final String ELEMENT_ANONYMOUS = "anonymous";
+    public static final String ELEMENT_VALUE = "value";
+    public static final String ELEMENT_USER = "user";
+    public static final String ELEMENT_EXCEPTIONS = "exceptions";
+    public static final String ELEMENT_CHAR_FILTERS = "char-filters";
+    public static final String ELEMENT_CHAR_FILTER = "char-filter";
+    public static final String ELEMENT_CHARACTER_PATTERN = "character-pattern";
+    public static final String ELEMENT_CATEGORY_URL = "category-url";
+    public static final String ELEMENT_ALLOWED_CONTEXT_PATHS = "allowed-context-paths";
+    public static final String ELEMENT_CATEGORY_NAME = "category-name";
+    public static final String ELEMENT_CATEGORY_URL_SUFFIX = "category-url-suffix";
+    public static final String SEO_CONFIG_FILENAME = "SeoConfig.xml";
+    public static final int DEFAULT_RESPONSECODE = HttpServletResponse.SC_MOVED_PERMANENTLY;
+    public static final String DEFAULT_ANONYMOUS_VALUE = "disable";
+    public static final String DEFAULT_USER_VALUE = "disable";
+    public static final String DEFAULT_CATEGORY_URL_VALUE = "enable";
+    public static final String DEFAULT_CATEGORY_NAME_VALUE = "disable";
+    public static final String ALLOWED_CONTEXT_PATHS_SEPERATOR = ":";
+
+    /**
+     * Initialize url regular express configuration.
+     * 
+     * @return result to indicate the status of initialization.
+     */
+    public static void init() {
+        FileInputStream configFileIS = null;
+        String result = "success";
+        seoPatterns = new HashMap<String, Pattern>();
+        seoReplacements = new HashMap<String, String>();
+        forwardReplacements = new HashMap<String, String>();
+        forwardResponseCodes = new HashMap<String, Integer>();
+        userExceptionPatterns = FastList.newInstance();
+        specialProductIds = FastMap.newInstance();
+        charFilters = FastMap.newInstance();
+        try {
+            URL seoConfigFilename = UtilURL.fromResource(SEO_CONFIG_FILENAME);
+            Document configDoc = UtilXml.readXmlDocument(seoConfigFilename, false);
+            Element rootElement = configDoc.getDocumentElement();
+
+            String regexIfMatch = UtilXml.childElementValue(rootElement, ELEMENT_REGEXPIFMATCH, DEFAULT_REGEXP);
+            Debug.logInfo("Parsing " + regexIfMatch, module);
+            try {
+                regexpIfMatch = perlCompiler.compile(regexIfMatch, Perl5Compiler.READ_ONLY_MASK);
+            } catch (MalformedPatternException e1) {
+                Debug.logWarning(e1, "Error while parsing " + regexIfMatch, module);
+            }
+
+            // parse category-url element
+            try {
+                Element categoryUrlElement = UtilXml.firstChildElement(rootElement, ELEMENT_CATEGORY_URL);
+                Debug.logInfo("Parsing " + ELEMENT_CATEGORY_URL + " [" + (categoryUrlElement != null) + "]:", module);
+                if (categoryUrlElement != null) {
+                    String enableCategoryUrlValue = UtilXml.childElementValue(categoryUrlElement, ELEMENT_VALUE, DEFAULT_CATEGORY_URL_VALUE);
+                    if (DEFAULT_CATEGORY_URL_VALUE.equalsIgnoreCase(enableCategoryUrlValue)) {
+                        categoryUrlEnabled = true;
+                    } else {
+                        categoryUrlEnabled = false;
+                    }
+                    
+                    if (categoryUrlEnabled) {
+                        String allowedContextValue = UtilXml.childElementValue(categoryUrlElement, ELEMENT_ALLOWED_CONTEXT_PATHS, null);
+                        allowedContextPaths = FastSet.newInstance();
+                        if (UtilValidate.isNotEmpty(allowedContextValue)) {
+                            List<String> allowedContextPathList = StringUtil.split(allowedContextValue, ALLOWED_CONTEXT_PATHS_SEPERATOR);
+                            for (String path : allowedContextPathList) {
+                                if (UtilValidate.isNotEmpty(path)) {
+                                    path = path.trim();
+                                    if (!allowedContextPaths.contains(path)) {
+                                        allowedContextPaths.add(path);
+                                        Debug.logInfo("  " + ELEMENT_ALLOWED_CONTEXT_PATHS + ": " + path, module);
+                                    }
+                                }
+                            }
+                        }
+                        
+                        String categoryNameValue = UtilXml.childElementValue(categoryUrlElement, ELEMENT_CATEGORY_NAME, DEFAULT_CATEGORY_NAME_VALUE);
+                        if (DEFAULT_CATEGORY_NAME_VALUE.equalsIgnoreCase(categoryNameValue)) {
+                            categoryNameEnabled = false;
+                        } else {
+                            categoryNameEnabled = true;
+                        }
+                        Debug.logInfo("  " + ELEMENT_CATEGORY_NAME + ": " + categoryNameEnabled, module);
+
+                        categoryUrlSuffix = UtilXml.childElementValue(categoryUrlElement, ELEMENT_CATEGORY_URL_SUFFIX, null);
+                        if (UtilValidate.isNotEmpty(categoryUrlSuffix)) {
+                            categoryUrlSuffix = categoryUrlSuffix.trim();
+                            if (categoryUrlSuffix.contains("/")) {
+                                categoryUrlSuffix = null;
+                            }
+                        }
+                        Debug.logInfo("  " + ELEMENT_CATEGORY_URL_SUFFIX + ": " + categoryUrlSuffix, module);
+                    }
+                }
+            } catch (NullPointerException e) {
+                // no "category-url" element
+                Debug.logWarning("No category-url element found in " + seoConfigFilename.toString(), module);
+            }
+
+            // parse jsessionid element
+            try {
+                Element jSessionId = UtilXml.firstChildElement(rootElement, ELEMENT_JSESSIONID);
+                Debug.logInfo("Parsing " + ELEMENT_JSESSIONID + " [" + (jSessionId != null) + "]:", module);
+                if (jSessionId != null) {
+                    Element anonymous = UtilXml.firstChildElement(jSessionId, ELEMENT_ANONYMOUS);
+                    if (anonymous != null) {
+                        String anonymousValue = UtilXml.childElementValue(anonymous, ELEMENT_VALUE, DEFAULT_ANONYMOUS_VALUE);
+                        if (DEFAULT_ANONYMOUS_VALUE.equalsIgnoreCase(anonymousValue)) {
+                            jSessionIdAnonEnabled = false;
+                        } else {
+                            jSessionIdAnonEnabled = true;
+                        }
+                    } else {
+                        jSessionIdAnonEnabled = Boolean.valueOf(DEFAULT_ANONYMOUS_VALUE).booleanValue();
+                    }
+                    Debug.logInfo("  " + ELEMENT_ANONYMOUS + ": " + jSessionIdAnonEnabled, module);
+                    
+                    Element user = UtilXml.firstChildElement(jSessionId, ELEMENT_USER);
+                    if (user != null) {
+                        String userValue = UtilXml.childElementValue(user, ELEMENT_VALUE, DEFAULT_USER_VALUE);
+                        if (DEFAULT_USER_VALUE.equalsIgnoreCase(userValue)) {
+                            jSessionIdUserEnabled = false;
+                        } else {
+                            jSessionIdUserEnabled = true;
+                        }
+
+                        Element exceptions = UtilXml.firstChildElement(user, ELEMENT_EXCEPTIONS);
+                        if (exceptions != null) {
+                            Debug.logInfo("  " + ELEMENT_EXCEPTIONS + ": ", module);
+                            List<? extends Element> exceptionUrlPatterns = UtilXml.childElementList(exceptions, ELEMENT_URLPATTERN);
+                            for (int i = 0; i < exceptionUrlPatterns.size(); i++) {
+                                Element element = exceptionUrlPatterns.get(i);
+                                String urlpattern = element.getTextContent();
+                                if (UtilValidate.isNotEmpty(urlpattern)) {
+                                    try {
+                                        Pattern pattern = perlCompiler.compile(urlpattern, Perl5Compiler.READ_ONLY_MASK);
+                                        userExceptionPatterns.add(pattern);
+                                        Debug.logInfo("    " + ELEMENT_URLPATTERN + ": " + urlpattern, module);
+                                    } catch (MalformedPatternException e) {
+                                        Debug.logWarning("Can NOT parse " + urlpattern + " in element " + ELEMENT_URLPATTERN + " of " + ELEMENT_EXCEPTIONS + ". Error: " + e.getMessage(), module);
+                                    }
+                                }
+                            }
+                        }
+                    } else {
+                        jSessionIdUserEnabled = Boolean.valueOf(DEFAULT_USER_VALUE).booleanValue();
+                    }
+                    Debug.logInfo("  " + ELEMENT_USER + ": " + jSessionIdUserEnabled, module);
+                }
+            } catch (NullPointerException e) {
+                Debug.logWarning("No jsessionid element found in " + seoConfigFilename.toString(), module);
+            }
+            
+            // parse url-config elements
+            try {
+                NodeList configs = rootElement.getElementsByTagName(ELEMENT_URL_CONFIG);
+                Debug.logInfo("Parsing " + ELEMENT_URL_CONFIG, module);
+                for (int j = 0; j < configs.getLength(); j++) {
+                    Element config = (Element) configs.item(j);
+                    String urlpattern = UtilXml.childElementValue(config, ELEMENT_URLPATTERN, null);
+                    if (UtilValidate.isEmpty(urlpattern)) {
+                        continue;
+                    }
+                    Debug.logInfo("  " + ELEMENT_URLPATTERN + ": " + urlpattern, module);
+                    Pattern pattern;
+                    try {
+                        pattern = perlCompiler.compile(urlpattern, Perl5Compiler.READ_ONLY_MASK);
+                        seoPatterns.put(urlpattern, pattern);
+                    } catch (MalformedPatternException e) {
+                        Debug.logWarning("Error while creating parttern for seo url-pattern: " + urlpattern, module);
+                        continue;
+                    }
+                    
+                    // construct seo patterns
+                    Element seo = UtilXml.firstChildElement(config, ELEMENT_SEO);
+                    if (UtilValidate.isNotEmpty(seo)) {
+                        String replacement = UtilXml.childElementValue(seo, ELEMENT_REPLACEMENT, null);
+                        if (UtilValidate.isNotEmpty(replacement)) {
+                            seoReplacements.put(urlpattern, replacement);
+                            Debug.logInfo("    " + ELEMENT_SEO + " " + ELEMENT_REPLACEMENT + ": " + replacement, module);
+                        }
+                    }
+
+                    // construct forward patterns
+                    Element forward = UtilXml.firstChildElement(config, ELEMENT_FORWARD);
+                    if (UtilValidate.isNotEmpty(forward)) {
+                        String replacement = UtilXml.childElementValue(forward, ELEMENT_REPLACEMENT, null);
+                        String responseCode = UtilXml.childElementValue(forward,
+                                ELEMENT_RESPONSECODE, String.valueOf(DEFAULT_RESPONSECODE));
+                        if (UtilValidate.isNotEmpty(replacement)) {
+                            forwardReplacements.put(urlpattern, replacement);
+                            Debug.logInfo("    " + ELEMENT_FORWARD + " " + ELEMENT_REPLACEMENT + ": " + replacement, module);
+                            if (UtilValidate.isNotEmpty(responseCode)) {
+                                Integer responseCodeInt = DEFAULT_RESPONSECODE;
+                                try {
+                                    responseCodeInt = Integer.valueOf(responseCode);
+                                } catch (NumberFormatException nfe) {
+                                    Debug.logWarning(nfe, "Error while parsing response code number: " + responseCode, module);
+                                }
+                                forwardResponseCodes.put(urlpattern, responseCodeInt);
+                                Debug.logInfo("    " + ELEMENT_FORWARD + " " + ELEMENT_RESPONSECODE + ": " + responseCodeInt, module);
+                            }
+                        }
+                    }
+                }
+            } catch (NullPointerException e) {
+                // no "url-config" element
+                Debug.logWarning("No " + ELEMENT_URL_CONFIG + " element found in " + seoConfigFilename.toString(), module);
+            }
+
+            // parse char-filters elements
+            try {
+                NodeList nameFilterNodes = rootElement
+                        .getElementsByTagName(ELEMENT_CHAR_FILTER);
+                Debug.logInfo("Parsing " + ELEMENT_CHAR_FILTER + ": ", module);
+                for (int i = 0; i < nameFilterNodes.getLength(); i++) {
+                    Element element = (Element) nameFilterNodes.item(i);
+                    String charaterPattern = UtilXml.childElementValue(element, ELEMENT_CHARACTER_PATTERN, null);
+                    String replacement = UtilXml.childElementValue(element, ELEMENT_REPLACEMENT, null);
+                    if (UtilValidate.isNotEmpty(charaterPattern)
+                            && UtilValidate.isNotEmpty(replacement)) {
+                        try {
+                            perlCompiler.compile(charaterPattern, Perl5Compiler.READ_ONLY_MASK);
+                            charFilters.put(charaterPattern, replacement);
+                            Debug.logInfo("  " + ELEMENT_CHARACTER_PATTERN + ": " + charaterPattern, module);
+                            Debug.logInfo("  " + ELEMENT_REPLACEMENT + ": " + replacement, module);
+                        } catch (MalformedPatternException e) {
+                            // skip this filter (character-pattern replacement) if any error happened
+                            Debug.logWarning(e, "Error while parsing " + ELEMENT_CHARACTER_PATTERN + ": " + charaterPattern, module);
+                        }
+                    }
+                }
+            } catch (NullPointerException e) {
+                // no "char-filters" element
+                Debug.logWarning("No " + ELEMENT_CHAR_FILTER + " element found in " + seoConfigFilename.toString(), module);
+            }
+        } catch (SAXException e) {
+            result = "error";
+            Debug.logError(e, module);
+        } catch (ParserConfigurationException e) {
+            result = "error";
+            Debug.logError(e, module);
+        } catch (IOException e) {
+            result = "error";
+            Debug.logError(e, module);
+        } finally {
+            if (configFileIS != null) {
+                try {
+                    configFileIS.close();
+                } catch (IOException e) {
+                    result = "error";
+                    Debug.logError(e, module);
+                }
+            }
+        }
+        if (seoReplacements.keySet().isEmpty()) {
+            useUrlRegexp = false;
+        } else {
+            useUrlRegexp = true;
+        }
+        if (result.equals("success")) {
+            isInitialed = true;
+        }
+    }
+    
+    /**
+     * Check whether the configuration file has been read.
+     * 
+     * @return a boolean value to indicate whether the configuration file has been read.
+     */
+    public static boolean isInitialed() {
+        return isInitialed;
+    }
+
+    /**
+     * Check whether url regexp should be used.
+     * 
+     * @return a boolean value to indicate whether url regexp should be used.
+     */
+    public static boolean checkUseUrlRegexp() {
+        return useUrlRegexp;
+    }
+
+    /**
+     * Get the general regexp pattern.
+     * 
+     * @return the general regexp pattern.
+     */
+    public static Pattern getGeneralRegexpPattern() {
+        return regexpIfMatch;
+    }
+    
+    /**
+     * Check whether category url is enabled.
+     * 
+     * @return a boolean value to indicate whether category url is enabled.
+     */
+    public static boolean checkCategoryUrl() {
+        return categoryUrlEnabled;
+    }
+
+    /**
+     * Check whether the context path is enabled.
+     * 
+     * @return a boolean value to indicate whether the context path is enabled.
+     */
+    public static boolean isCategoryUrlEnabled(String contextPath) {
+        if (contextPath == null) {
+            return false;
+        }
+        if (UtilValidate.isEmpty(contextPath)) {
+            contextPath = "/";
+        }
+        if (categoryUrlEnabled) {
+            if (allowedContextPaths.contains(contextPath.trim())) {
+                return true;
+            } else {
+                return false;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Check whether category name is enabled.
+     * 
+     * @return a boolean value to indicate whether category name is enabled.
+     */
+    public static boolean isCategoryNameEnabled() {
+        return categoryNameEnabled;
+    }
+
+    /**
+     * Get category url suffix.
+     * 
+     * @return String category url suffix.
+     */
+    public static String getCategoryUrlSuffix() {
+        return categoryUrlSuffix;
+    }
+
+    /**
+     * Check whether jsessionid is enabled for anonymous.
+     * 
+     * @return a boolean value to indicate whether jsessionid is enabled for anonymous.
+     */
+    public static boolean isJSessionIdAnonEnabled() {
+        return jSessionIdAnonEnabled;
+    }
+
+    /**
+     * Check whether jsessionid is enabled for user.
+     * 
+     * @return a boolean value to indicate whether jsessionid is enabled for user.
+     */
+    public static boolean isJSessionIdUserEnabled() {
+        return jSessionIdUserEnabled;
+    }
+
+    /**
+     * Get user exception url pattern configures.
+     * 
+     * @return user exception url pattern configures (java.util.List<Pattern>)
+     */
+    public static List<Pattern> getUserExceptionPatterns() {
+        return userExceptionPatterns;
+    }
+
+    /**
+     * Get char filters.
+     * 
+     * @return char filters (java.util.Map<String, String>)
+     */
+    public static Map<String, String> getCharFilters() {
+        return charFilters;
+    }
+
+    /**
+     * Get seo url pattern configures.
+     * 
+     * @return seo url pattern configures (java.util.Map<String, Pattern>)
+     */
+    public static Map<String, Pattern> getSeoPatterns() {
+        return seoPatterns;
+    }
+
+    /**
+     * Get seo replacement configures.
+     * 
+     * @return seo replacement configures (java.util.Map<String, String>)
+     */
+    public static Map<String, String> getSeoReplacements() {
+        return seoReplacements;
+    }
+
+    /**
+     * Get forward replacement configures.
+     * 
+     * @return forward replacement configures (java.util.Map<String, String>)
+     */
+    public static Map<String, String> getForwardReplacements() {
+        return forwardReplacements;
+    }
+
+    /**
+     * Get forward response codes.
+     * 
+     * @return forward response code configures (java.util.Map<String, Integer>)
+     */
+    public static Map<String, Integer> getForwardResponseCodes() {
+        return forwardResponseCodes;
+    }
+
+    /**
+     * Check whether a product id is in the special list. If we cannot get a product from a lower cased
+     * or upper cased product id, then it's special.
+     * 
+     * @return boolean to indicate whether the product id is special.
+     */
+    @Deprecated
+    public static boolean isSpecialProductId(String productId) {
+        return specialProductIds.containsKey(productId);
+    }
+
+    /**
+     * Add a special product id to the special list.
+     * 
+     * @param productId a product id get from database.
+     * @return true to indicate it has been added to special product id; false to indicate it's not special.
+     * @throws Exception to indicate there's already same lower cased product id in the list but value is a different product id.
+     */
+    @Deprecated
+    public static boolean addSpecialProductId(String productId) throws Exception {
+        if (productId.toLowerCase().equals(productId) || productId.toUpperCase().equals(productId)) {
+            return false;
+        }
+        if (isSpecialProductId(productId.toLowerCase())) {
+            if (specialProductIds.containsValue(productId)) {
+                return true;
+            } else {
+                throw new Exception("This product Id cannot be lower cased for SEO URL purpose: " + productId);
+            }
+        }
+        specialProductIds.put(productId.toLowerCase(), productId);
+        return true;
+    }
+    
+    /**
+     * Get a product id is in the special list.
+     * 
+     * @return String of the original product id
+     */
+    @Deprecated
+    public static String getSpecialProductId(String productId) {
+        return specialProductIds.get(productId);
+    }
+}
diff --git a/applications/product/src/org/ofbiz/product/category/SeoContentUrlFilter.java b/applications/product/src/org/ofbiz/product/category/SeoContentUrlFilter.java
new file mode 100644
index 0000000..f8ae6c2
--- /dev/null
+++ b/applications/product/src/org/ofbiz/product/category/SeoContentUrlFilter.java
@@ -0,0 +1,171 @@
+/*******************************************************************************
+ * 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.
+ *******************************************************************************/
+
+package org.ofbiz.product.category;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Set;
+
+import javax.servlet.FilterChain;
+import javax.servlet.RequestDispatcher;
+import javax.servlet.ServletContext;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import javolution.util.FastList;
+
+import org.ofbiz.base.util.Debug;
+import org.ofbiz.base.util.StringUtil;
+import org.ofbiz.base.util.UtilHttp;
+import org.ofbiz.base.util.UtilMisc;
+import org.ofbiz.base.util.UtilValidate;
+import org.ofbiz.common.UrlServletHelper;
+import org.ofbiz.entity.Delegator;
+import org.ofbiz.entity.GenericValue;
+import org.ofbiz.entity.condition.EntityCondition;
+import org.ofbiz.entity.condition.EntityOperator;
+import org.ofbiz.entity.util.EntityUtil;
+import org.ofbiz.webapp.control.ContextFilter;
+import org.owasp.esapi.errors.EncodingException;
+
+public class SeoContentUrlFilter extends ContextFilter {
+    public final static String module = SeoContentUrlFilter.class.getName();
+    protected static String defaultLocaleString = null;
+    protected static String redirectUrl = null;
+    public static String defaultViewRequest = "contentViewInfo";
+
+    @Override
+    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
+        HttpServletRequest httpRequest = (HttpServletRequest) request;
+        HttpServletResponse httpResponse = (HttpServletResponse) response;
+        Delegator delegator = (Delegator) httpRequest.getSession().getServletContext().getAttribute("delegator");
+
+        // Get ServletContext
+        ServletContext servletContext = config.getServletContext();
+        // Set request attribute and session
+        UrlServletHelper.setRequestAttributes(request, delegator, servletContext);
+        String urlContentId = null;
+        String pathInfo = UtilHttp.getFullRequestUrl(httpRequest);
+        if (UtilValidate.isNotEmpty(pathInfo)) {
+            String alternativeUrl = pathInfo.substring(pathInfo.lastIndexOf("/"));
+            if (alternativeUrl.endsWith("-content")) {
+                try {
+                    List<GenericValue> contentDataResourceViews = delegator.findByAnd("ContentDataResourceView", UtilMisc.toMap("drObjectInfo", alternativeUrl), null, false);
+                    if (contentDataResourceViews.size() > 0) {
+                        contentDataResourceViews = EntityUtil.orderBy(contentDataResourceViews, UtilMisc.toList("createdDate DESC"));
+                        GenericValue contentDataResourceView = EntityUtil.getFirst(contentDataResourceViews);
+                        List<GenericValue> contents = EntityUtil.filterByDate(delegator.findByAnd("ContentAssoc",
+                                UtilMisc.toMap("contentAssocTypeId", "ALTERNATIVE_URL", "contentIdTo", contentDataResourceView.getString("contentId")), null, false));
+                        if (contents.size() > 0) {
+                            GenericValue content = EntityUtil.getFirst(contents);
+                            urlContentId = content.getString("contentId");
+                        }
+                    }
+                } catch (Exception e) {
+                    Debug.logWarning(e.getMessage(), module);
+                }
+            }
+            if (UtilValidate.isNotEmpty(urlContentId)) {
+                StringBuilder urlBuilder = new StringBuilder();
+                if (UtilValidate.isNotEmpty(SeoControlServlet.controlServlet)) {
+                    urlBuilder.append("/" + SeoControlServlet.controlServlet);
+                }
+                urlBuilder.append("/" + config.getInitParameter("viewRequest") + "?contentId=" + urlContentId);
+
+                // Set view query parameters
+                UrlServletHelper.setViewQueryParameters(request, urlBuilder);
+                Debug.logInfo("[Filtered request]: " + pathInfo + " (" + urlBuilder + ")", module);
+                RequestDispatcher dispatch = request.getRequestDispatcher(urlBuilder.toString());
+                dispatch.forward(request, response);
+                return;
+            }
+
+            // Check path alias
+            UrlServletHelper.checkPathAlias(request, httpResponse, delegator, pathInfo);
+        }
+        // we're done checking; continue on
+        chain.doFilter(request, response);
+    }
+
+    public static String makeContentAltUrl(HttpServletRequest request, HttpServletResponse response, String contentId, String viewContent) {
+        if (UtilValidate.isEmpty(contentId)) {
+            return null;
+        }
+        Delegator delegator = (Delegator) request.getAttribute("delegator");
+        String url = null;
+        try {
+            List<EntityCondition> expr = FastList.newInstance();
+            expr.add(EntityCondition.makeCondition("caContentAssocTypeId", EntityOperator.EQUALS, "ALTERNATIVE_URL"));
+            expr.add(EntityCondition.makeCondition("caThruDate", EntityOperator.EQUALS, null));
+            expr.add(EntityCondition.makeCondition("contentIdStart", EntityOperator.EQUALS, contentId));
+            Set<String> fieldsToSelect = UtilMisc.toSet("contentIdStart", "drObjectInfo", "dataResourceId", "caFromDate", "caThruDate", "caCreatedDate");
+            List<GenericValue> contentAssocDataResources = delegator.findList("ContentAssocDataResourceViewTo", EntityCondition.makeCondition(expr), fieldsToSelect,
+                    UtilMisc.toList("-caFromDate"), null, true);
+            if (contentAssocDataResources.size() > 0) {
+                GenericValue contentAssocDataResource = EntityUtil.getFirst(contentAssocDataResources);
+                url = contentAssocDataResource.getString("drObjectInfo");
+                try {
+                    url = StringUtil.defaultWebEncoder.decodeFromURL(url);
+                    String mountPoint = request.getContextPath();
+                    if (!(mountPoint.equals("/")) && !(mountPoint.equals(""))) {
+                        url = mountPoint + url;
+                    }
+                } catch (EncodingException e) {
+                    Debug.logError(e, module);
+                }
+            }
+        } catch (Exception e) {
+            Debug.logWarning("[Exception] : " + e.getMessage(), module);
+        }
+
+        if (UtilValidate.isEmpty(url)) {
+            if (UtilValidate.isEmpty(viewContent)) {
+                viewContent = defaultViewRequest;
+            }
+            url = makeContentUrl(request, response, contentId, viewContent);
+        }
+        return url;
+    }
+
+    public static String makeContentUrl(HttpServletRequest request, HttpServletResponse response, String contentId, String viewContent) {
+        if (UtilValidate.isEmpty(contentId)) {
+            return null;
+        }
+        StringBuilder urlBuilder = new StringBuilder();
+        urlBuilder.append(request.getSession().getServletContext().getContextPath());
+        if (urlBuilder.charAt(urlBuilder.length() - 1) != '/') {
+            urlBuilder.append("/");
+        }
+        if (UtilValidate.isNotEmpty(SeoControlServlet.controlServlet)) {
+            urlBuilder.append(SeoControlServlet.controlServlet + "/");
+        }
+
+        if (UtilValidate.isNotEmpty(viewContent)) {
+            urlBuilder.append(viewContent);
+        } else {
+            urlBuilder.append(defaultViewRequest);
+        }
+        urlBuilder.append("?contentId=" + contentId);
+        return urlBuilder.toString();
+    }
+}
diff --git a/applications/product/src/org/ofbiz/product/category/SeoContextFilter.java b/applications/product/src/org/ofbiz/product/category/SeoContextFilter.java
new file mode 100644
index 0000000..0530eea
--- /dev/null
+++ b/applications/product/src/org/ofbiz/product/category/SeoContextFilter.java
@@ -0,0 +1,395 @@
+/*******************************************************************************
+ * 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.
+ *******************************************************************************/
+package org.ofbiz.product.category;
+
+import static org.ofbiz.base.util.UtilGenerics.checkMap;
+
+import java.io.IOException;
+import java.net.URL;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.RequestDispatcher;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRegistration;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import javolution.util.FastSet;
+
+import org.apache.oro.text.regex.Pattern;
+import org.apache.oro.text.regex.Perl5Matcher;
+import org.ofbiz.base.util.Debug;
+import org.ofbiz.base.util.StringUtil;
+import org.ofbiz.base.util.UtilHttp;
+import org.ofbiz.base.util.UtilMisc;
+import org.ofbiz.base.util.UtilObject;
+import org.ofbiz.base.util.UtilProperties;
+import org.ofbiz.base.util.UtilValidate;
+import org.ofbiz.entity.Delegator;
+import org.ofbiz.entity.DelegatorFactory;
+import org.ofbiz.entity.GenericEntityException;
+import org.ofbiz.entity.GenericValue;
+import org.ofbiz.entity.condition.EntityCondition;
+import org.ofbiz.entity.util.EntityUtil;
+import org.ofbiz.security.Security;
+import org.ofbiz.service.LocalDispatcher;
+import org.ofbiz.webapp.control.ConfigXMLReader;
+import org.ofbiz.webapp.control.ConfigXMLReader.ControllerConfig;
+import org.ofbiz.webapp.control.ContextFilter;
+import org.ofbiz.webapp.control.WebAppConfigurationException;
+import org.ofbiz.webapp.website.WebSiteWorker;
+
+/**
+ * SeoContextFilter - Restricts access to raw files and configures servlet objects.
+ */
+public class SeoContextFilter extends ContextFilter {
+
+    public static final String module = SeoContextFilter.class.getName();
+    
+    protected Set<String> WebServlets = FastSet.newInstance();
+    
+    public void init(FilterConfig config) throws ServletException {
+        super.init(config);
+        
+        Map<String, ? extends ServletRegistration> servletRegistrations = config.getServletContext().getServletRegistrations();
+        for (String key : servletRegistrations.keySet()) {
+            Collection<String> servlets = servletRegistrations.get(key).getMappings();
+            for (String servlet : servlets) {
+                if (servlet.endsWith("/*")) {
+                    servlet = servlet.substring(0, servlet.length() - 2);
+                    if (UtilValidate.isNotEmpty(servlet) && !WebServlets.contains(servlet)) {
+                        WebServlets.add(servlet);
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * @see javax.servlet.Filter#doFilter(javax.servlet.ServletRequest, javax.servlet.ServletResponse, javax.servlet.FilterChain)
+     */
+    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
+        HttpServletRequest httpRequest = (HttpServletRequest) request;
+        HttpServletResponse httpResponse = (HttpServletResponse) response;
+
+        String uri = httpRequest.getRequestURI();
+        boolean forwarded = forwardUri(httpResponse, uri);
+        if (forwarded) {
+            return;
+        }
+
+        URL controllerConfigURL = ConfigXMLReader.getControllerConfigURL(config.getServletContext());
+        ControllerConfig controllerConfig = null;
+        Map<String, ConfigXMLReader.RequestMap> requestMaps = null;
+        try {
+            controllerConfig = ConfigXMLReader.getControllerConfig(controllerConfigURL);
+            requestMaps = controllerConfig.getRequestMapMap();
+        } catch (WebAppConfigurationException e) {
+            Debug.logError(e, "Exception thrown while parsing controller.xml file: ", module);
+            throw new ServletException(e);
+        }
+        Set<String> uris = requestMaps.keySet();
+
+        // NOTE: the following part is copied from org.ofbiz.webapp.control.ContextFilter.doFilter method, please update this if framework is updated.
+        // Debug.logInfo("Running ContextFilter.doFilter", module);
+
+        // ----- Servlet Object Setup -----
+        // set the cached class loader for more speedy running in this thread
+
+        // set the ServletContext in the request for future use
+        httpRequest.setAttribute("servletContext", config.getServletContext());
+
+        // set the webSiteId in the session
+        if (UtilValidate.isEmpty(httpRequest.getSession().getAttribute("webSiteId"))) {
+            httpRequest.getSession().setAttribute("webSiteId", WebSiteWorker.getWebSiteId(httpRequest));
+        }
+
+        // set the filesystem path of context root.
+        httpRequest.setAttribute("_CONTEXT_ROOT_", config.getServletContext().getRealPath("/"));
+
+        // set the server root url
+        String serverRootUrl = UtilHttp.getServerRootUrl(httpRequest);
+        httpRequest.setAttribute("_SERVER_ROOT_URL_", serverRootUrl);
+
+        // request attributes from redirect call
+        String reqAttrMapHex = (String) httpRequest.getSession().getAttribute("_REQ_ATTR_MAP_");
+        if (UtilValidate.isNotEmpty(reqAttrMapHex)) {
+            byte[] reqAttrMapBytes = StringUtil.fromHexString(reqAttrMapHex);
+            Map<String, Object> reqAttrMap = checkMap(UtilObject.getObject(reqAttrMapBytes), String.class, Object.class);
+            if (reqAttrMap != null) {
+                for (Map.Entry<String, Object> entry : reqAttrMap.entrySet()) {
+                    httpRequest.setAttribute(entry.getKey(), entry.getValue());
+                }
+            }
+            httpRequest.getSession().removeAttribute("_REQ_ATTR_MAP_");
+        }
+
+        // ----- Context Security -----
+        // check if we are disabled
+        String disableSecurity = config.getInitParameter("disableContextSecurity");
+        if (disableSecurity != null && "Y".equalsIgnoreCase(disableSecurity)) {
+            chain.doFilter(httpRequest, httpResponse);
+            return;
+        }
+
+        // check if we are told to redirect everthing
+        String redirectAllTo = config.getInitParameter("forceRedirectAll");
+        if (UtilValidate.isNotEmpty(redirectAllTo)) {
+            // little trick here so we don't loop on ourself
+            if (httpRequest.getSession().getAttribute("_FORCE_REDIRECT_") == null) {
+                httpRequest.getSession().setAttribute("_FORCE_REDIRECT_", "true");
+                Debug.logWarning("Redirecting user to: " + redirectAllTo, module);
+
+                if (!redirectAllTo.toLowerCase().startsWith("http")) {
+                    redirectAllTo = httpRequest.getContextPath() + redirectAllTo;
+                }
+                httpResponse.sendRedirect(redirectAllTo);
+                return;
+            } else {
+                httpRequest.getSession().removeAttribute("_FORCE_REDIRECT_");
+                chain.doFilter(httpRequest, httpResponse);
+                return;
+            }
+        }
+
+        // test to see if we have come through the control servlet already, if not do the processing
+        String requestPath = null;
+        String contextUri = null;
+        if (httpRequest.getAttribute(ContextFilter.FORWARDED_FROM_SERVLET) == null) {
+            // Debug.logInfo("In ContextFilter.doFilter, FORWARDED_FROM_SERVLET is NOT set", module);
+            String allowedPath = config.getInitParameter("allowedPaths");
+            String redirectPath = config.getInitParameter("redirectPath");
+            String errorCode = config.getInitParameter("errorCode");
+
+            List<String> allowList = StringUtil.split(allowedPath, ":");
+
+            if (debug) Debug.logInfo("[Domain]: " + httpRequest.getServerName() + " [Request]: " + httpRequest.getRequestURI(), module);
+
+            requestPath = httpRequest.getServletPath();
+            if (requestPath == null) requestPath = "";
+            if (requestPath.lastIndexOf("/") > 0) {
+                if (requestPath.indexOf("/") == 0) {
+                    requestPath = "/" + requestPath.substring(1, requestPath.indexOf("/", 1));
+                } else {
+                    requestPath = requestPath.substring(1, requestPath.indexOf("/"));
+                }
+            }
+
+            String requestInfo = httpRequest.getServletPath();
+            if (requestInfo == null) requestInfo = "";
+            if (requestInfo.lastIndexOf("/") >= 0) {
+                requestInfo = requestInfo.substring(0, requestInfo.lastIndexOf("/")) + "/*";
+            }
+
+            StringBuilder contextUriBuffer = new StringBuilder();
+            if (httpRequest.getContextPath() != null) {
+                contextUriBuffer.append(httpRequest.getContextPath());
+            }
+            if (httpRequest.getServletPath() != null) {
+                contextUriBuffer.append(httpRequest.getServletPath());
+            }
+            if (httpRequest.getPathInfo() != null) {
+                contextUriBuffer.append(httpRequest.getPathInfo());
+            }
+            contextUri = contextUriBuffer.toString();
+
+            List<String> pathItemList = StringUtil.split(httpRequest.getPathInfo(), "/");
+            String viewName = "";
+            if (pathItemList != null) {
+                viewName = pathItemList.get(0);
+            }
+            
+            String requestUri = UtilHttp.getRequestUriFromTarget(httpRequest.getRequestURI());
+
+            // Verbose Debugging
+            if (Debug.verboseOn()) {
+                for (String allow : allowList) {
+                    Debug.logVerbose("[Allow]: " + allow, module);
+                }
+                Debug.logVerbose("[View Name]: " + viewName, module);
+                Debug.logVerbose("[Request Uri]: " + requestUri, module);
+                Debug.logVerbose("[Request path]: " + requestPath, module);
+                Debug.logVerbose("[Request info]: " + requestInfo, module);
+                Debug.logVerbose("[Servlet path]: " + httpRequest.getServletPath(), module);
+                Debug.logVerbose(
+                        "[Not In AllowList]: " + (!allowList.contains(requestPath) && !allowList.contains(requestInfo) && !allowList.contains(httpRequest.getServletPath()) && !allowList.contains(requestUri) && !allowList.contains("/" + viewName)),
+                        module);
+                Debug.logVerbose("[Not In controller]: " + (UtilValidate.isEmpty(requestPath) && UtilValidate.isEmpty(httpRequest.getServletPath()) && !uris.contains(viewName)),
+                        module);
+            }
+
+            // check to make sure the requested url is allowed
+            if (!allowList.contains(requestPath) && !allowList.contains(requestInfo) && !allowList.contains(httpRequest.getServletPath())
+                    && !allowList.contains(requestUri) && !allowList.contains("/" + viewName)
+                    && (UtilValidate.isEmpty(requestPath) && UtilValidate.isEmpty(httpRequest.getServletPath()) && !uris.contains(viewName))) {
+                String filterMessage = "[Filtered request]: " + contextUri;
+
+                if (redirectPath == null) {
+                    if (UtilValidate.isEmpty(viewName)) {
+                        // redirect without any url change in browser
+                        RequestDispatcher rd = request.getRequestDispatcher(SeoControlServlet.defaultPage);
+                        rd.forward(request, response);
+                    } else {
+                        int error = 404;
+                        if (UtilValidate.isNotEmpty(errorCode)) {
+                            try {
+                                error = Integer.parseInt(errorCode);
+                            } catch (NumberFormatException nfe) {
+                                Debug.logWarning(nfe, "Error code specified would not parse to Integer : " + errorCode, module);
+                            }
+                        }
+                        filterMessage = filterMessage + " (" + error + ")";
+                        httpResponse.sendError(error, contextUri);
+                        request.setAttribute("filterRequestUriError", contextUri);
+                    }
+                } else {
+                    filterMessage = filterMessage + " (" + redirectPath + ")";
+                    if (!redirectPath.toLowerCase().startsWith("http")) {
+                        redirectPath = httpRequest.getContextPath() + redirectPath;
+                    }
+                    // httpResponse.sendRedirect(redirectPath);
+                    if (uri.equals("") || uri.equals("/")) {
+                        // redirect without any url change in browser
+                        RequestDispatcher rd = request.getRequestDispatcher(redirectPath);
+                        rd.forward(request, response);
+                    } else {
+                        // redirect with url change in browser
+                        httpResponse.setStatus(SeoConfigUtil.DEFAULT_RESPONSECODE);
+                        httpResponse.setHeader("Location", redirectPath);
+                    }
+                }
+                Debug.logWarning(filterMessage, module);
+                return;
+            } else if ((allowList.contains(requestPath) || allowList.contains(requestInfo) || allowList.contains(httpRequest.getServletPath())
+                    || allowList.contains(requestUri) || allowList.contains("/" + viewName))
+                    && !WebServlets.contains(httpRequest.getServletPath())) {
+                request.setAttribute(SeoControlServlet.REQUEST_IN_ALLOW_LIST, Boolean.TRUE);
+            }
+        }
+
+        // check if multi tenant is enabled
+        String useMultitenant = UtilProperties.getPropertyValue("general.properties", "multitenant");
+        if ("Y".equals(useMultitenant)) {
+            // get tenant delegator by domain name
+            String serverName = httpRequest.getServerName();
+            try {
+                // if tenant was specified, replace delegator with the new per-tenant delegator and set tenantId to session attribute
+                Delegator delegator = getDelegator(config.getServletContext());
+                List<GenericValue> tenants = delegator.findList("Tenant", EntityCondition.makeCondition("domainName", serverName), null, UtilMisc.toList("-createdStamp"), null, false);
+                if (UtilValidate.isNotEmpty(tenants)) {
+                    GenericValue tenant = EntityUtil.getFirst(tenants);
+                    String tenantId = tenant.getString("tenantId");
+
+                    // if the request path is a root mount then redirect to the initial path
+                    if (UtilValidate.isNotEmpty(requestPath) && requestPath.equals(contextUri)) {
+                        String initialPath = tenant.getString("initialPath");
+                        if (UtilValidate.isNotEmpty(initialPath) && !"/".equals(initialPath)) {
+                            ((HttpServletResponse) response).sendRedirect(initialPath);
+                            return;
+                        }
+                    }
+
+                    // make that tenant active, setup a new delegator and a new dispatcher
+                    String tenantDelegatorName = delegator.getDelegatorBaseName() + "#" + tenantId;
+                    httpRequest.getSession().setAttribute("delegatorName", tenantDelegatorName);
+
+                    // after this line the delegator is replaced with the new per-tenant delegator
+                    delegator = DelegatorFactory.getDelegator(tenantDelegatorName);
+                    config.getServletContext().setAttribute("delegator", delegator);
+
+                    // clear web context objects
+                    config.getServletContext().setAttribute("security", null);
+                    config.getServletContext().setAttribute("dispatcher", null);
+
+                    // initialize security
+                    Security security = getSecurity();
+                    // initialize the services dispatcher
+                    LocalDispatcher dispatcher = getDispatcher(config.getServletContext());
+
+                    // set web context objects
+                    request.setAttribute("dispatcher", dispatcher);
+                    request.setAttribute("security", security);
+
+                    request.setAttribute("tenantId", tenantId);
+                }
+
+                // NOTE DEJ20101130: do NOT always put the delegator name in the user's session because the user may
+                // have logged in and specified a tenant, and even if no Tenant record with a matching domainName field
+                // is found this will change the user's delegator back to the base one instead of the one for the
+                // tenant specified on login
+                // httpRequest.getSession().setAttribute("delegatorName", delegator.getDelegatorName());
+            } catch (GenericEntityException e) {
+                Debug.logWarning(e, "Unable to get Tenant", module);
+            }
+        }
+
+        // we're done checking; continue on
+        chain.doFilter(httpRequest, httpResponse);
+    }
+
+    /**
+     * Forward a uri according to forward pattern regular expressions. Note: this is developed for Filter usage.
+     * 
+     * @param uri String to reverse transform
+     * @return String
+     */
+    protected static boolean forwardUri(HttpServletResponse response, String uri) {
+        Perl5Matcher matcher = new Perl5Matcher();
+        boolean foundMatch = false;
+        Integer responseCodeInt = null;
+
+        if (SeoConfigUtil.checkUseUrlRegexp() && SeoConfigUtil.getSeoPatterns() != null && SeoConfigUtil.getForwardReplacements() != null) {
+            Iterator<String> keys = SeoConfigUtil.getSeoPatterns().keySet().iterator();
+            while (keys.hasNext()) {
+                String key = keys.next();
+                Pattern pattern = SeoConfigUtil.getSeoPatterns().get(key);
+                String replacement = SeoConfigUtil.getForwardReplacements().get(key);
+                if (matcher.matches(uri, pattern)) {
+                    for (int i = matcher.getMatch().groups(); i > 0; i--) {
+                        replacement = replacement.replaceAll("\\$" + i, matcher.getMatch().group(i));
+                    }
+                    uri = replacement;
+                    responseCodeInt = SeoConfigUtil.getForwardResponseCodes().get(key);
+                    foundMatch = true;
+                    // be careful, we don't break after finding a match
+                }
+            }
+        }
+
+        if (foundMatch) {
+            if (responseCodeInt == null) {
+                response.setStatus(SeoConfigUtil.DEFAULT_RESPONSECODE);
+            } else {
+                response.setStatus(responseCodeInt.intValue());
+            }
+            response.setHeader("Location", uri);
+        } else {
+            Debug.logInfo("Can NOT forward this url: " + uri, module);
+        }
+        return foundMatch;
+    }
+}
diff --git a/applications/product/src/org/ofbiz/product/category/SeoControlServlet.java b/applications/product/src/org/ofbiz/product/category/SeoControlServlet.java
new file mode 100644
index 0000000..817ed1a
--- /dev/null
+++ b/applications/product/src/org/ofbiz/product/category/SeoControlServlet.java
@@ -0,0 +1,91 @@
+/*******************************************************************************
+ * 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.
+ *******************************************************************************/
+package org.ofbiz.product.category;
+
+import java.io.IOException;
+import java.net.URLEncoder;
+
+import javax.servlet.ServletConfig;
+import javax.servlet.ServletContext;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.catalina.servlets.DefaultServlet;
+import org.apache.jasper.servlet.JspServlet;
+import org.ofbiz.base.util.Debug;
+import org.ofbiz.base.util.UtilValidate;
+import org.ofbiz.webapp.control.ControlServlet;
+
+/**
+ * SeoControlServlet.java - SEO Master servlet for the web application.
+ */
+@SuppressWarnings("serial")
+public class SeoControlServlet extends ControlServlet {
+
+    public static final String module = SeoControlServlet.class.getName();
+
+    protected static String defaultPage = null;
+    protected static String controlServlet = null;
+    
+    public static final String REQUEST_IN_ALLOW_LIST = "_REQUEST_IN_ALLOW_LIST_";
+
+    public SeoControlServlet() {
+        super();
+    }
+
+    /**
+     * @see javax.servlet.Servlet#init(javax.servlet.ServletConfig)
+     */
+    public void init(ServletConfig config) throws ServletException {
+        super.init(config);
+
+        ServletContext context = this.getServletContext();
+        if (UtilValidate.isEmpty(defaultPage)) {
+            defaultPage = context.getInitParameter("defaultPage");
+        }
+        if (UtilValidate.isEmpty(defaultPage)) {
+            defaultPage = "/main";
+        }
+
+        if (defaultPage.startsWith("/") && defaultPage.lastIndexOf("/") > 0) {
+            controlServlet = defaultPage.substring(1);
+            controlServlet = controlServlet.substring(0, controlServlet.indexOf("/"));
+        }
+
+        SeoConfigUtil.init();
+    }
+    
+    public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
+		String uri = URLEncoder.encode(request.getRequestURI(), "UTF-8");
+        if (request.getAttribute(REQUEST_IN_ALLOW_LIST) != null || request.getAttribute("_jsp_" + uri) != null) {
+            if (request.getRequestURI().toLowerCase().endsWith(".jsp") || request.getRequestURI().toLowerCase().endsWith(".jspx") ) {
+                JspServlet jspServlet = new JspServlet();
+                jspServlet.init(this.getServletConfig());
+                jspServlet.service(request, response);
+            } else {
+                DefaultServlet defaultServlet = new DefaultServlet();
+                defaultServlet.init(this.getServletConfig());
+                defaultServlet.service(request, response);
+            }
+            return;
+        }
+        super.doGet(request, response);
+    }
+}
diff --git a/applications/product/src/org/ofbiz/product/category/SeoUrlUtil.java b/applications/product/src/org/ofbiz/product/category/SeoUrlUtil.java
new file mode 100644
index 0000000..66540dc
--- /dev/null
+++ b/applications/product/src/org/ofbiz/product/category/SeoUrlUtil.java
@@ -0,0 +1,43 @@
+/*******************************************************************************
+ * 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.
+ *******************************************************************************/
+package org.ofbiz.product.category;
+
+import org.ofbiz.base.util.UtilValidate;
+
+public class SeoUrlUtil {
+    public static String replaceSpecialCharsUrl(String url) {
+        if (UtilValidate.isEmpty(url)) {
+            url = "";
+        }
+        for (String characterPattern : SeoConfigUtil.getCharFilters().keySet()) {
+            url = url.replaceAll(characterPattern, SeoConfigUtil.getCharFilters().get(characterPattern));
+        }
+        return url;
+    }
+
+    public static String removeContextPath(String uri, String contextPath) {
+        if (UtilValidate.isEmpty(contextPath) || UtilValidate.isEmpty(uri)) {
+            return uri;
+        }
+        if (uri.length() > contextPath.length() && uri.startsWith(contextPath)) {
+            return uri.substring(contextPath.length());
+        }
+        return uri;
+    }
+}
diff --git a/applications/product/src/org/ofbiz/product/category/UrlRegexpContextFilter.java b/applications/product/src/org/ofbiz/product/category/UrlRegexpContextFilter.java
new file mode 100644
index 0000000..ed21da3
--- /dev/null
+++ b/applications/product/src/org/ofbiz/product/category/UrlRegexpContextFilter.java
@@ -0,0 +1,322 @@
+/*******************************************************************************
+ * 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.
+ *******************************************************************************/
+package org.ofbiz.product.category;
+
+import static org.ofbiz.base.util.UtilGenerics.checkMap;
+
+import java.io.IOException;
+import java.net.URL;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.servlet.FilterChain;
+import javax.servlet.RequestDispatcher;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.ofbiz.base.util.Debug;
+import org.ofbiz.base.util.GeneralException;
+import org.ofbiz.base.util.StringUtil;
+import org.ofbiz.base.util.UtilHttp;
+import org.ofbiz.base.util.UtilMisc;
+import org.ofbiz.base.util.UtilObject;
+import org.ofbiz.base.util.UtilProperties;
+import org.ofbiz.base.util.UtilValidate;
+import org.ofbiz.entity.Delegator;
+import org.ofbiz.entity.DelegatorFactory;
+import org.ofbiz.entity.GenericEntityException;
+import org.ofbiz.entity.GenericValue;
+import org.ofbiz.entity.condition.EntityCondition;
+import org.ofbiz.entity.util.EntityUtil;
+import org.ofbiz.product.category.ftl.UrlRegexpTransform;
+import org.ofbiz.security.Security;
+import org.ofbiz.service.LocalDispatcher;
+import org.ofbiz.webapp.control.ConfigXMLReader;
+import org.ofbiz.webapp.control.ConfigXMLReader.ControllerConfig;
+import org.ofbiz.webapp.control.ContextFilter;
+import org.ofbiz.webapp.control.WebAppConfigurationException;
+
+/**
+ * UrlRegexpContextFilter - Restricts access to raw files and configures servlet objects.
+ */
+public class UrlRegexpContextFilter extends ContextFilter {
+
+    public static final String module = UrlRegexpContextFilter.class.getName();
+
+    /**
+     * @throws GeneralException 
+     * @see javax.servlet.Filter#doFilter(javax.servlet.ServletRequest, javax.servlet.ServletResponse, javax.servlet.FilterChain)
+     */
+    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
+        HttpServletRequest httpRequest = (HttpServletRequest) request;
+        HttpServletResponse httpResponse = (HttpServletResponse) response;
+
+        String uri = httpRequest.getRequestURI();
+        boolean forwarded = UrlRegexpTransform.forwardUri(httpResponse, uri);
+        if (forwarded) {
+            return;
+        }
+        
+        URL controllerConfigURL = ConfigXMLReader.getControllerConfigURL(config.getServletContext());
+        ControllerConfig controllerConfig = null;
+        Map<String, ConfigXMLReader.RequestMap> requestMaps = null;
+        try {
+            controllerConfig = ConfigXMLReader.getControllerConfig(controllerConfigURL);
+            requestMaps = controllerConfig.getRequestMapMap();
+        } catch (WebAppConfigurationException e) {
+            Debug.logError(e, "Exception thrown while parsing controller.xml file: ", module);
+            throw new ServletException(e);
+        }
+        Set<String> uris = requestMaps.keySet();
+
+        // NOTE: the following part is copied from org.ofbiz.webapp.control.ContextFilter.doFilter method, please update this if framework is updated.
+        // Debug.logInfo("Running ContextFilter.doFilter", module);
+
+        // ----- Servlet Object Setup -----
+        // set the cached class loader for more speedy running in this thread
+
+
+        // set the ServletContext in the request for future use
+        httpRequest.setAttribute("servletContext", config.getServletContext());
+
+        // set the webSiteId in the session
+        if (UtilValidate.isEmpty(httpRequest.getSession().getAttribute("webSiteId"))) {
+            httpRequest.getSession().setAttribute("webSiteId", config.getServletContext().getAttribute("webSiteId"));
+        }
+
+        // set the filesystem path of context root.
+        httpRequest.setAttribute("_CONTEXT_ROOT_", config.getServletContext().getRealPath("/"));
+
+        // set the server root url
+        String serverRootUrl = UtilHttp.getServerRootUrl(httpRequest);
+        httpRequest.setAttribute("_SERVER_ROOT_URL_", serverRootUrl);
+
+        // request attributes from redirect call
+        String reqAttrMapHex = (String) httpRequest.getSession().getAttribute("_REQ_ATTR_MAP_");
+        if (UtilValidate.isNotEmpty(reqAttrMapHex)) {
+            byte[] reqAttrMapBytes = StringUtil.fromHexString(reqAttrMapHex);
+            Map<String, Object> reqAttrMap = checkMap(UtilObject.getObject(reqAttrMapBytes), String.class, Object.class);
+            if (reqAttrMap != null) {
+                for (Map.Entry<String, Object> entry : reqAttrMap.entrySet()) {
+                    httpRequest.setAttribute(entry.getKey(), entry.getValue());
+                }
+            }
+            httpRequest.getSession().removeAttribute("_REQ_ATTR_MAP_");
+        }
+
+        // ----- Context Security -----
+        // check if we are disabled
+        String disableSecurity = config.getInitParameter("disableContextSecurity");
+        if (disableSecurity != null && "Y".equalsIgnoreCase(disableSecurity)) {
+            chain.doFilter(httpRequest, httpResponse);
+            return;
+        }
+
+        // check if we are told to redirect everthing
+        String redirectAllTo = config.getInitParameter("forceRedirectAll");
+        if (UtilValidate.isNotEmpty(redirectAllTo)) {
+            // little trick here so we don't loop on ourself
+            if (httpRequest.getSession().getAttribute("_FORCE_REDIRECT_") == null) {
+                httpRequest.getSession().setAttribute("_FORCE_REDIRECT_", "true");
+                Debug.logWarning("Redirecting user to: " + redirectAllTo, module);
+
+                if (!redirectAllTo.toLowerCase().startsWith("http")) {
+                    redirectAllTo = httpRequest.getContextPath() + redirectAllTo;
+                }
+                httpResponse.sendRedirect(redirectAllTo);
+                return;
+            } else {
+                httpRequest.getSession().removeAttribute("_FORCE_REDIRECT_");
+                chain.doFilter(httpRequest, httpResponse);
+                return;
+            }
+        }
+
+        // test to see if we have come through the control servlet already, if not do the processing
+        String requestPath = null;
+        String contextUri = null;
+        if (httpRequest.getAttribute(ContextFilter.FORWARDED_FROM_SERVLET) == null) {
+            if (debug) Debug.log("[Domain]: " + httpRequest.getServerName() + " [Request]: " + httpRequest.getRequestURI(), module);
+
+            requestPath = httpRequest.getServletPath();
+            if (requestPath == null) requestPath = "";
+            if (requestPath.lastIndexOf("/") > 0) {
+                if (requestPath.indexOf("/") == 0) {
+                    requestPath = "/" + requestPath.substring(1, requestPath.indexOf("/", 1));
+                } else {
+                    requestPath = requestPath.substring(1, requestPath.indexOf("/"));
+                }
+            }
+
+            String requestInfo = httpRequest.getServletPath();
+            if (requestInfo == null) requestInfo = "";
+            if (requestInfo.lastIndexOf("/") >= 0) {
+                requestInfo = requestInfo.substring(0, requestInfo.lastIndexOf("/")) + "/*";
+            }
+
+            StringBuilder contextUriBuffer = new StringBuilder();
+            if (httpRequest.getContextPath() != null) {
+                contextUriBuffer.append(httpRequest.getContextPath());
+            }
+            if (httpRequest.getServletPath() != null) {
+                contextUriBuffer.append(httpRequest.getServletPath());
+            }
+            if (httpRequest.getPathInfo() != null) {
+                contextUriBuffer.append(httpRequest.getPathInfo());
+            }
+            contextUri = contextUriBuffer.toString();
+
+            List<String> pathItemList = StringUtil.split(httpRequest.getPathInfo(), "/");
+            String viewName = "";
+            if (pathItemList != null) {
+                viewName = pathItemList.get(0);
+            }
+
+            // Debug.logInfo("In ContextFilter.doFilter, FORWARDED_FROM_SERVLET is NOT set", module);
+            String allowedPath = config.getInitParameter("allowedPaths");
+            String redirectPath = config.getInitParameter("redirectPath");
+            String errorCode = config.getInitParameter("errorCode");
+
+            List<String> allowList = StringUtil.split(allowedPath, ":");
+            allowList.add("/"); // No path is allowed.
+            if (UtilValidate.isNotEmpty(httpRequest.getServletPath())) {
+                allowList.add(""); // No path is allowed if servlet path is not empty.
+            }
+
+            // Verbose Debugging
+            if (Debug.verboseOn()) {
+                for (String allow : allowList) {
+                    Debug.logVerbose("[Allow]: " + allow, module);
+                }
+                Debug.logVerbose("[Request path]: " + requestPath, module);
+                Debug.logVerbose("[Request info]: " + requestInfo, module);
+                Debug.logVerbose("[Servlet path]: " + httpRequest.getServletPath(), module);
+                Debug.logVerbose("[View name]: " + viewName, module);
+                Debug.logVerbose("[Not In AllowList]: " + (!allowList.contains(requestPath) && !allowList.contains(requestInfo) && !allowList.contains(httpRequest.getServletPath())), module);
+                Debug.logVerbose("[Not In controller]: " + (UtilValidate.isEmpty(requestPath) && UtilValidate.isEmpty(httpRequest.getServletPath()) && !uris.contains(viewName)), module);
+            }
+
+            // check to make sure the requested url is allowed
+            if (!allowList.contains(requestPath) && !allowList.contains(requestInfo) && !allowList.contains(httpRequest.getServletPath())
+                    && (UtilValidate.isEmpty(requestPath) && UtilValidate.isEmpty(httpRequest.getServletPath()) && !uris.contains(viewName))) {
+                String filterMessage = "[Filtered request]: " + contextUri;
+
+                if (redirectPath == null) {
+                    int error = 404;
+                    if (UtilValidate.isNotEmpty(errorCode)) {
+                        try {
+                            error = Integer.parseInt(errorCode);
+                        } catch (NumberFormatException nfe) {
+                            Debug.logWarning(nfe, "Error code specified would not parse to Integer : " + errorCode, module);
+                        }
+                    }
+                    filterMessage = filterMessage + " (" + error + ")";
+                    httpResponse.sendError(error, contextUri);
+                } else {
+                    filterMessage = filterMessage + " (" + redirectPath + ")";
+                    if (!redirectPath.toLowerCase().startsWith("http")) {
+                        redirectPath = httpRequest.getContextPath() + redirectPath;
+                    }
+                    // httpResponse.sendRedirect(redirectPath);
+                    if (uri.equals("") || uri.equals("/")) {
+                        // redirect without any url change in browser
+                        RequestDispatcher rd = request.getRequestDispatcher(redirectPath);
+                        rd.forward(request, response);
+                    } else {
+                        // redirect with url change in browser
+                        httpResponse.setStatus(SeoConfigUtil.DEFAULT_RESPONSECODE);
+                        httpResponse.setHeader("Location", redirectPath);
+                    }
+                }
+                Debug.logWarning(filterMessage, module);
+                return;
+            }
+        }
+
+        // check if multi tenant is enabled
+        String useMultitenant = UtilProperties.getPropertyValue("general.properties", "multitenant");
+        if ("Y".equals(useMultitenant)) {
+            // get tenant delegator by domain name
+            String serverName = httpRequest.getServerName();
+            try {
+                // if tenant was specified, replace delegator with the new per-tenant delegator and set tenantId to session attribute
+                Delegator delegator = getDelegator(config.getServletContext());
+                List<GenericValue> tenants = delegator.findList("Tenant", EntityCondition.makeCondition("domainName", serverName), null, UtilMisc.toList("-createdStamp"), null, false);
+                if (UtilValidate.isNotEmpty(tenants)) {
+                    GenericValue tenant = EntityUtil.getFirst(tenants);
+                    String tenantId = tenant.getString("tenantId");
+
+                    // if the request path is a root mount then redirect to the initial path
+                    if (UtilValidate.isNotEmpty(requestPath) && requestPath.equals(contextUri)) {
+                        String initialPath = tenant.getString("initialPath");
+                        if (UtilValidate.isNotEmpty(initialPath) && !"/".equals(initialPath)) {
+                            ((HttpServletResponse) response).sendRedirect(initialPath);
+                            return;
+                        }
+                    }
+
+                    // make that tenant active, setup a new delegator and a new dispatcher
+                    String tenantDelegatorName = delegator.getDelegatorBaseName() + "#" + tenantId;
+                    httpRequest.getSession().setAttribute("delegatorName", tenantDelegatorName);
+
+                    // after this line the delegator is replaced with the new per-tenant delegator
+                    delegator = DelegatorFactory.getDelegator(tenantDelegatorName);
+                    config.getServletContext().setAttribute("delegator", delegator);
+
+                    // clear web context objects
+                    // config.getServletContext().setAttribute("authorization", null);
+                    config.getServletContext().setAttribute("security", null);
+                    config.getServletContext().setAttribute("dispatcher", null);
+
+                    // initialize authorizer
+                    // getAuthz();
+                    // initialize security
+                    Security security = getSecurity();
+                    // initialize the services dispatcher
+                    LocalDispatcher dispatcher = getDispatcher(config.getServletContext());
+
+                    // set web context objects
+                    httpRequest.getSession().setAttribute("dispatcher", dispatcher);
+                    httpRequest.getSession().setAttribute("security", security);
+
+                    httpRequest.setAttribute("tenantId", tenantId);
+                }
+
+                // NOTE DEJ20101130: do NOT always put the delegator name in the user's session because the user may
+                // have logged in and specified a tenant, and even if no Tenant record with a matching domainName field
+                // is found this will change the user's delegator back to the base one instead of the one for the
+                // tenant specified on login
+                // httpRequest.getSession().setAttribute("delegatorName", delegator.getDelegatorName());
+            } catch (GenericEntityException e) {
+                Debug.logWarning(e, "Unable to get Tenant", module);
+            }
+        }
+
+        // we're done checking; continue on
+        chain.doFilter(httpRequest, httpResponse);
+
+        // reset thread local security
+        // AbstractAuthorization.clearThreadLocal();
+    }
+
+}
diff --git a/applications/product/src/org/ofbiz/product/category/OfbizCatalogAltUrlTransform.java b/applications/product/src/org/ofbiz/product/category/ftl/CatalogAltUrlSeoTransform.java
similarity index 60%
copy from applications/product/src/org/ofbiz/product/category/OfbizCatalogAltUrlTransform.java
copy to applications/product/src/org/ofbiz/product/category/ftl/CatalogAltUrlSeoTransform.java
index f8f6373..0aebcdb 100644
--- a/applications/product/src/org/ofbiz/product/category/OfbizCatalogAltUrlTransform.java
+++ b/applications/product/src/org/ofbiz/product/category/ftl/CatalogAltUrlSeoTransform.java
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  *******************************************************************************/
-package org.ofbiz.product.category;
+package org.ofbiz.product.category.ftl;
 
 import java.io.IOException;
 import java.io.Writer;
@@ -25,13 +25,16 @@
 
 import javax.servlet.http.HttpServletRequest;
 
+import org.ofbiz.base.util.Debug;
 import org.ofbiz.base.util.UtilMisc;
 import org.ofbiz.base.util.UtilValidate;
 import org.ofbiz.base.util.template.FreeMarkerWorker;
 import org.ofbiz.entity.Delegator;
 import org.ofbiz.entity.GenericEntityException;
 import org.ofbiz.entity.GenericValue;
-import org.ofbiz.entity.util.EntityQuery;
+import org.ofbiz.product.category.CatalogUrlFilter;
+import org.ofbiz.product.category.CategoryContentWrapper;
+import org.ofbiz.product.category.SeoConfigUtil;
 import org.ofbiz.product.product.ProductContentWrapper;
 import org.ofbiz.service.LocalDispatcher;
 import org.ofbiz.webapp.OfbizUrlBuilder;
@@ -46,10 +49,9 @@
 import freemarker.template.TemplateModelException;
 import freemarker.template.TemplateTransformModel;
 
-public class OfbizCatalogAltUrlTransform implements TemplateTransformModel {
-    public final static String module = OfbizCatalogUrlTransform.class.getName();
+public class CatalogAltUrlSeoTransform implements TemplateTransformModel {
+    public final static String module = CatalogUrlSeoTransform.class.getName();
 
-    @SuppressWarnings("unchecked")
     public String getStringArg(Map args, String key) {
         Object o = args.get(key);
         if (o instanceof SimpleScalar) {
@@ -78,26 +80,21 @@
     }
 
     @Override
-    @SuppressWarnings("unchecked")
-    public Writer getWriter(final Writer out, final Map args)
-            throws TemplateModelException, IOException {
+    public Writer getWriter(final Writer out, final Map args) throws TemplateModelException, IOException {
         final StringBuilder buf = new StringBuilder();
         final boolean fullPath = checkArg(args, "fullPath", false);
         final boolean secure = checkArg(args, "secure", false);
 
         return new Writer(out) {
-            
-            @Override
+
             public void write(char[] cbuf, int off, int len) throws IOException {
                 buf.append(cbuf, off, len);
             }
-            
-            @Override
+
             public void flush() throws IOException {
                 out.flush();
             }
-            
-            @Override
+
             public void close() throws IOException {
                 try {
                     Environment env = Environment.getCurrentEnvironment();
@@ -106,7 +103,7 @@
                     String productCategoryId = getStringArg(args, "productCategoryId");
                     String productId = getStringArg(args, "productId");
                     String url = "";
-                    
+
                     Object prefix = env.getVariable("urlPrefix");
                     String viewSize = getStringArg(args, "viewSize");
                     String viewIndex = getStringArg(args, "viewIndex");
@@ -116,14 +113,26 @@
                         HttpServletRequest request = (HttpServletRequest) req.getWrappedObject();
                         StringBuilder newURL = new StringBuilder();
                         if (UtilValidate.isNotEmpty(productId)) {
-                            url = CatalogUrlFilter.makeProductUrl(request, previousCategoryId, productCategoryId, productId);
+                            if (SeoConfigUtil.isCategoryUrlEnabled(request.getContextPath())) {
+                                url = CatalogUrlSeoTransform.makeProductUrl(request, productId, productCategoryId, previousCategoryId);
+                            } else {
+                                url = CatalogUrlFilter.makeProductUrl(request, previousCategoryId, productCategoryId, productId);
+                            }
                         } else {
-                            url = CatalogUrlFilter.makeCategoryUrl(request, previousCategoryId, productCategoryId, productId, viewSize, viewIndex, viewSort, searchString);
+                            if (SeoConfigUtil.isCategoryUrlEnabled(request.getContextPath())) {
+                                url = CatalogUrlSeoTransform.makeCategoryUrl(request, productCategoryId, previousCategoryId, viewSize, viewIndex, viewSort, searchString);
+                            } else {
+                                url = CatalogUrlFilter.makeCategoryUrl(request, previousCategoryId, productCategoryId, productId, viewSize, viewIndex, viewSort, searchString);
+                            }
                         }
                         // make the link
-                        if (fullPath){
-                            OfbizUrlBuilder builder = OfbizUrlBuilder.from(request);
-                            builder.buildHostPart(newURL, url, secure);
+                        if (fullPath) {
+                            try {
+                                OfbizUrlBuilder builder = OfbizUrlBuilder.from(request);
+                                builder.buildHostPart(newURL, "", secure);
+                            } catch (WebAppConfigurationException e) {
+                                Debug.logError(e.getMessage(), module);
+                            }
                         }
                         newURL.append(url);
                         out.write(newURL.toString());
@@ -131,14 +140,31 @@
                         Delegator delegator = FreeMarkerWorker.getWrappedObject("delegator", env);
                         LocalDispatcher dispatcher = FreeMarkerWorker.getWrappedObject("dispatcher", env);
                         Locale locale = (Locale) args.get("locale");
+                        String prefixString = ((StringModel) prefix).getAsString();
+                        prefixString = prefixString.replaceAll("&#47;", "/");
+                        String contextPath = prefixString;
+                        int lastSlashIndex = prefixString.lastIndexOf("/");
+                        if (lastSlashIndex > -1 && lastSlashIndex < prefixString.length()) {
+                            contextPath = prefixString.substring(prefixString.lastIndexOf("/"));
+                        }
                         if (UtilValidate.isNotEmpty(productId)) {
-                            GenericValue product = EntityQuery.use(delegator).from("Product").where("productId", productId).queryOne();
+                            GenericValue product = delegator.findOne("Product", UtilMisc.toMap("productId", productId), false);
                             ProductContentWrapper wrapper = new ProductContentWrapper(dispatcher, product, locale, "text/html");
-                            url = CatalogUrlFilter.makeProductUrl(delegator, wrapper, null, ((StringModel) prefix).getAsString(), previousCategoryId, productCategoryId, productId);
+                            if (SeoConfigUtil.isCategoryUrlEnabled(contextPath)) {
+                                url = CatalogUrlSeoTransform.makeProductUrl(delegator, wrapper, prefixString, contextPath, productCategoryId, previousCategoryId, productId);
+                            } else {
+                                url = CatalogUrlFilter.makeProductUrl(delegator, wrapper, null, prefixString, previousCategoryId, productCategoryId,
+                                        productId);
+                            }
                         } else {
-                            GenericValue productCategory = EntityQuery.use(delegator).from("ProductCategory").where("productCategoryId", productCategoryId).queryOne();
+                            GenericValue productCategory = delegator.findOne("ProductCategory", UtilMisc.toMap("productCategoryId", productCategoryId), false);
                             CategoryContentWrapper wrapper = new CategoryContentWrapper(dispatcher, productCategory, locale, "text/html");
-                            url = CatalogUrlFilter.makeCategoryUrl(delegator, wrapper, null, ((StringModel) prefix).getAsString(), previousCategoryId, productCategoryId, productId, viewSize, viewIndex, viewSort, searchString);
+                            if (SeoConfigUtil.isCategoryUrlEnabled(contextPath)) {
+                                url = CatalogUrlSeoTransform.makeCategoryUrl(delegator, wrapper, prefixString, productCategoryId, previousCategoryId, productId, viewSize, viewIndex, viewSort, searchString);
+                            } else {
+                                url = CatalogUrlFilter.makeCategoryUrl(delegator, wrapper, null, prefixString, previousCategoryId, productCategoryId,
+                                        productId, viewSize, viewIndex, viewSort, searchString);
+                            }
                         }
                         out.write(url.toString());
                     } else {
@@ -148,8 +174,6 @@
                     throw new IOException(e.getMessage());
                 } catch (GenericEntityException e) {
                     throw new IOException(e.getMessage());
-                } catch (WebAppConfigurationException e) {
-                    throw new IOException(e.getMessage());
                 }
             }
         };
diff --git a/applications/product/src/org/ofbiz/product/category/ftl/CatalogUrlSeoTransform.java b/applications/product/src/org/ofbiz/product/category/ftl/CatalogUrlSeoTransform.java
new file mode 100644
index 0000000..ed9c3b0
--- /dev/null
+++ b/applications/product/src/org/ofbiz/product/category/ftl/CatalogUrlSeoTransform.java
@@ -0,0 +1,840 @@
+/*******************************************************************************
+ * 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.
+ *******************************************************************************/
+package org.ofbiz.product.category.ftl;
+
+import java.io.IOException;
+import java.io.Writer;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+import javax.servlet.RequestDispatcher;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import javolution.util.FastList;
+import javolution.util.FastMap;
+
+import org.apache.oro.text.regex.MalformedPatternException;
+import org.apache.oro.text.regex.Pattern;
+import org.apache.oro.text.regex.Perl5Compiler;
+import org.apache.oro.text.regex.Perl5Matcher;
+import org.ofbiz.base.util.Debug;
+import org.ofbiz.base.util.StringUtil;
+import org.ofbiz.base.util.StringUtil.StringWrapper;
+import org.ofbiz.base.util.UtilMisc;
+import org.ofbiz.base.util.UtilValidate;
+import org.ofbiz.common.UrlServletHelper;
+import org.ofbiz.entity.Delegator;
+import org.ofbiz.entity.GenericEntityException;
+import org.ofbiz.entity.GenericValue;
+import org.ofbiz.entity.condition.EntityCondition;
+import org.ofbiz.entity.condition.EntityExpr;
+import org.ofbiz.entity.condition.EntityOperator;
+import org.ofbiz.product.category.CatalogUrlServlet;
+import org.ofbiz.product.category.CategoryContentWrapper;
+import org.ofbiz.product.category.CategoryWorker;
+import org.ofbiz.product.category.SeoConfigUtil;
+import org.ofbiz.product.category.SeoUrlUtil;
+import org.ofbiz.product.product.ProductContentWrapper;
+
+import freemarker.core.Environment;
+import freemarker.ext.beans.BeanModel;
+import freemarker.ext.beans.StringModel;
+import freemarker.template.SimpleScalar;
+import freemarker.template.TemplateModelException;
+import freemarker.template.TemplateTransformModel;
+
+public class CatalogUrlSeoTransform implements TemplateTransformModel {
+    public final static String module = CatalogUrlSeoTransform.class.getName();
+    
+    private static Map<String, String> categoryNameIdMap = null;
+    private static Map<String, String> categoryIdNameMap = null;
+    private static boolean categoryMapInitialed = false;
+    private static final String asciiRegexp = "^[0-9-_a-zA-Z]*$";
+    private static Pattern asciiPattern = null;
+    public static final String URL_HYPHEN = "-";
+
+    static {
+        if (!SeoConfigUtil.isInitialed()) {
+            SeoConfigUtil.init();
+        }
+        try {
+            Perl5Compiler perlCompiler = new Perl5Compiler();
+            asciiPattern = perlCompiler.compile(asciiRegexp, Perl5Compiler.READ_ONLY_MASK);
+        } catch (MalformedPatternException e1) {
+            Debug.logWarning(e1, module);
+        }
+    }
+    
+    public String getStringArg(Map args, String key) {
+        Object o = args.get(key);
+        if (o instanceof SimpleScalar) {
+            return ((SimpleScalar) o).getAsString();
+        } else if (o instanceof StringModel) {
+            return ((StringModel) o).getAsString();
+        }
+        return null;
+    }
+
+    @Override
+    public Writer getWriter(final Writer out, final Map args)
+            throws TemplateModelException, IOException {
+        final StringBuilder buf = new StringBuilder();
+        
+        return new Writer(out) {
+            
+            @Override
+            public void write(char[] cbuf, int off, int len) throws IOException {
+                buf.append(cbuf, off, len);
+            }
+            
+            @Override
+            public void flush() throws IOException {
+                out.flush();
+            }
+            
+            @Override
+            public void close() throws IOException {
+                try {
+                    Environment env = Environment.getCurrentEnvironment();
+                    BeanModel req = (BeanModel) env.getVariable("request");
+                    if (req != null) {
+                        String productId = getStringArg(args, "productId");
+                        String currentCategoryId = getStringArg(args, "currentCategoryId");
+                        String previousCategoryId = getStringArg(args, "previousCategoryId");
+                        HttpServletRequest request = (HttpServletRequest) req.getWrappedObject();
+                        
+                        if (!isCategoryMapInitialed()) {
+                            initCategoryMap(request);
+                        }
+
+                        String catalogUrl = "";
+                        if (SeoConfigUtil.isCategoryUrlEnabled(request.getContextPath())) {
+                            if (UtilValidate.isEmpty(productId)) {
+                                catalogUrl = makeCategoryUrl(request, currentCategoryId, previousCategoryId, null, null, null, null);
+                            } else {
+                                catalogUrl = makeProductUrl(request, productId, currentCategoryId, previousCategoryId);
+                            }
+                        } else {
+                            catalogUrl = CatalogUrlServlet.makeCatalogUrl(request, productId, currentCategoryId, previousCategoryId);
+                        }
+                        out.write(catalogUrl);
+                    }
+                } catch (TemplateModelException e) {
+                    throw new IOException(e.getMessage());
+                }
+            }
+        };
+    }
+    
+    /**
+     * Check whether the category map is initialed.
+     * 
+     * @return a boolean value to indicate whether the category map has been initialized.
+     */
+    public static boolean isCategoryMapInitialed() {
+        return categoryMapInitialed;
+    }
+    
+    /**
+     * Get the category name/id map.
+     * 
+     * @return the category name/id map
+     */
+    public static Map<String, String> getCategoryNameIdMap() {
+        return categoryNameIdMap;
+    }
+    
+    /**
+     * Get the category id/name map.
+     * 
+     * @return the category id/name map
+     */
+    public static Map<String, String> getCategoryIdNameMap() {
+        return categoryIdNameMap;
+    }
+    
+    /**
+     * Initial category-name/category-id map.
+     * Note: as a key, the category-name should be:
+     *         1. ascii
+     *         2. lower cased and use hyphen between the words.
+     *       If not, the category id will be used.
+     * 
+     */
+    public static synchronized void initCategoryMap(HttpServletRequest request) {
+        Delegator delegator = (Delegator) request.getAttribute("delegator");
+        initCategoryMap(request, delegator);
+    }
+    
+    public static synchronized void initCategoryMap(HttpServletRequest request, Delegator delegator) {
+        if (SeoConfigUtil.checkCategoryUrl()) {
+            categoryNameIdMap = FastMap.newInstance();
+            categoryIdNameMap = FastMap.newInstance();
+            Perl5Matcher matcher = new Perl5Matcher();
+
+            try {
+                Collection<GenericValue> allCategories = delegator.findList("ProductCategory", null, UtilMisc.toSet("productCategoryId", "categoryName"), null, null, false);
+                for (GenericValue category : allCategories) {
+                    String categoryName = category.getString("categoryName");
+                    String categoryNameId = null;
+                    String categoryIdName = null;
+                    String categoryId = category.getString("productCategoryId");
+                    if (UtilValidate.isNotEmpty(categoryName)) {
+                        categoryName = SeoUrlUtil.replaceSpecialCharsUrl(categoryName.trim());
+                        if (matcher.matches(categoryName, asciiPattern)) {
+                            categoryIdName = categoryName.replaceAll(" ", URL_HYPHEN);
+                            categoryNameId = categoryIdName + URL_HYPHEN + categoryId.trim().replaceAll(" ", URL_HYPHEN);
+                        } else {
+                            categoryIdName = categoryId.trim().replaceAll(" ", URL_HYPHEN);
+                            categoryNameId = categoryIdName;
+                        }
+                    } else {
+                        GenericValue productCategory = delegator.findOne("ProductCategory", UtilMisc.toMap("productCategoryId", categoryId), true);
+                        CategoryContentWrapper wrapper = new CategoryContentWrapper(productCategory, request);
+                        StringWrapper alternativeUrl = wrapper.get("ALTERNATIVE_URL");
+                        if (UtilValidate.isNotEmpty(alternativeUrl) && UtilValidate.isNotEmpty(alternativeUrl.toString())) {
+                            categoryIdName = SeoUrlUtil.replaceSpecialCharsUrl(alternativeUrl.toString());
+                            categoryNameId = categoryIdName + URL_HYPHEN + categoryId.trim().replaceAll(" ", URL_HYPHEN);
+                        } else {
+                            categoryNameId = categoryId.trim().replaceAll(" ", URL_HYPHEN);
+                            categoryIdName = categoryNameId;
+                        }
+                    }
+                    if (categoryNameIdMap.containsKey(categoryNameId)) {
+                        categoryNameId = categoryId.trim().replaceAll(" ", URL_HYPHEN);
+                        categoryIdName = categoryNameId;
+                    }
+                    if (!matcher.matches(categoryNameId, asciiPattern) || categoryNameIdMap.containsKey(categoryNameId)) {
+                        continue;
+                    }
+                    categoryNameIdMap.put(categoryNameId, categoryId);
+                    categoryIdNameMap.put(categoryId, categoryIdName);
+                }
+            } catch (GenericEntityException e) {
+                Debug.logError(e, module);
+            }
+        }
+        categoryMapInitialed = true;
+    }
+
+    /**
+     * Make product url according to the configurations.
+     * 
+     * @return String a catalog url
+     */
+    public static String makeProductUrl(HttpServletRequest request, String productId, String currentCategoryId, String previousCategoryId) {
+        Delegator delegator = (Delegator) request.getAttribute("delegator");
+        if (!isCategoryMapInitialed()) {
+            initCategoryMap(request);
+        }
+
+        String contextPath = request.getContextPath();
+        StringBuilder urlBuilder = new StringBuilder();
+        GenericValue product = null;
+        urlBuilder.append((request.getSession().getServletContext()).getContextPath());
+        if (urlBuilder.charAt(urlBuilder.length() - 1) != '/') {
+            urlBuilder.append("/");
+        }
+        if (UtilValidate.isNotEmpty(productId)) {
+            try {
+                product = delegator.findOne("Product", UtilMisc.toMap("productId", productId), true);
+            } catch (GenericEntityException e) {
+                Debug.logError(e, "Error looking up product info for productId [" + productId + "]: " + e.toString(), module);
+            }
+        }
+        if (product != null) {
+            urlBuilder.append(CatalogUrlServlet.PRODUCT_REQUEST + "/");
+        }
+
+        if (UtilValidate.isNotEmpty(currentCategoryId)) {
+            List<String> trail = CategoryWorker.getTrail(request);
+            trail = CategoryWorker.adjustTrail(trail, currentCategoryId, previousCategoryId);
+            if (!SeoConfigUtil.isCategoryUrlEnabled(contextPath)) {
+                for (String trailCategoryId: trail) {
+                    if ("TOP".equals(trailCategoryId)) continue;
+                    urlBuilder.append("/");
+                    urlBuilder.append(trailCategoryId);
+                }
+            } else {
+                if (trail.size() > 1) {
+                    String lastCategoryId = trail.get(trail.size() - 1);
+                    if (!"TOP".equals(lastCategoryId)) {
+                        if (SeoConfigUtil.isCategoryNameEnabled()) {
+                            String categoryName = CatalogUrlSeoTransform.getCategoryIdNameMap().get(lastCategoryId);
+                            if (UtilValidate.isNotEmpty(categoryName)) {
+                                urlBuilder.append(categoryName);
+                                if (product != null) {
+                                    urlBuilder.append(URL_HYPHEN);
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        }
+
+        if (UtilValidate.isNotEmpty(productId)) {
+            if (product != null) {
+                String productName = product.getString("productName");
+                productName = SeoUrlUtil.replaceSpecialCharsUrl(productName);
+                if (UtilValidate.isNotEmpty(productName)) {
+                    urlBuilder.append(productName + URL_HYPHEN);
+                } else {
+                    ProductContentWrapper wrapper = new ProductContentWrapper(product, request);
+                    StringWrapper alternativeUrl = wrapper.get("ALTERNATIVE_URL");
+                    if (UtilValidate.isNotEmpty(alternativeUrl) && UtilValidate.isNotEmpty(alternativeUrl.toString())) {
+                        productName = SeoUrlUtil.replaceSpecialCharsUrl(alternativeUrl.toString());
+                        if (UtilValidate.isNotEmpty(productName)) {
+                            urlBuilder.append(productName + URL_HYPHEN);
+                        }
+                    }
+                }
+            }
+            try {
+                //SeoConfigUtil.addSpecialProductId(productId);
+                urlBuilder.append(productId);
+            } catch (Exception e) {
+                urlBuilder.append(productId);
+            }
+        }
+        
+        if (!urlBuilder.toString().endsWith("/") && UtilValidate.isNotEmpty(SeoConfigUtil.getCategoryUrlSuffix())) {
+            urlBuilder.append(SeoConfigUtil.getCategoryUrlSuffix());
+        }
+        
+        return urlBuilder.toString();
+    }
+
+    /**
+     * Make category url according to the configurations.
+     * 
+     * @return String a category url
+     */
+    public static String makeCategoryUrl(HttpServletRequest request, String currentCategoryId, String previousCategoryId, String viewSize, String viewIndex, String viewSort, String searchString) {
+
+        if (!isCategoryMapInitialed()) {
+            initCategoryMap(request);
+        }
+
+        StringBuilder urlBuilder = new StringBuilder();
+        urlBuilder.append((request.getSession().getServletContext()).getContextPath());
+        if (urlBuilder.charAt(urlBuilder.length() - 1) != '/') {
+            urlBuilder.append("/");
+        }
+        urlBuilder.append(CatalogUrlServlet.CATEGORY_REQUEST + "/");
+
+        if (UtilValidate.isNotEmpty(currentCategoryId)) {
+            List<String> trail = CategoryWorker.getTrail(request);
+            trail = CategoryWorker.adjustTrail(trail, currentCategoryId, previousCategoryId);
+            if (trail.size() > 1) {
+                String lastCategoryId = trail.get(trail.size() - 1);
+                if (!"TOP".equals(lastCategoryId)) {
+                    String categoryName = CatalogUrlSeoTransform.getCategoryIdNameMap().get(lastCategoryId);
+                    if (UtilValidate.isNotEmpty(categoryName)) {
+                        urlBuilder.append(categoryName);
+                        urlBuilder.append(URL_HYPHEN);
+                        urlBuilder.append(lastCategoryId.trim().replaceAll(" ", URL_HYPHEN));
+                    } else {
+                        urlBuilder.append(lastCategoryId.trim().replaceAll(" ", URL_HYPHEN));
+                    }
+                }
+            }
+        }
+
+        if (!urlBuilder.toString().endsWith("/") && UtilValidate.isNotEmpty(SeoConfigUtil.getCategoryUrlSuffix())) {
+            urlBuilder.append(SeoConfigUtil.getCategoryUrlSuffix());
+        }
+        
+        // append view index
+        if (UtilValidate.isNotEmpty(viewIndex)) {
+            if (!urlBuilder.toString().endsWith("?") && !urlBuilder.toString().endsWith("&")) {
+                urlBuilder.append("?");
+            }
+            urlBuilder.append("viewIndex=" + viewIndex + "&");
+        }
+        // append view size
+        if (UtilValidate.isNotEmpty(viewSize)) {
+            if (!urlBuilder.toString().endsWith("?") && !urlBuilder.toString().endsWith("&")) {
+                urlBuilder.append("?");
+            }
+            urlBuilder.append("viewSize=" + viewSize + "&");
+        }
+        // append view sort
+        if (UtilValidate.isNotEmpty(viewSort)) {
+            if (!urlBuilder.toString().endsWith("?") && !urlBuilder.toString().endsWith("&")) {
+                urlBuilder.append("?");
+            }
+            urlBuilder.append("viewSort=" + viewSort + "&");
+        }
+        // append search string
+        if (UtilValidate.isNotEmpty(searchString)) {
+            if (!urlBuilder.toString().endsWith("?") && !urlBuilder.toString().endsWith("&")) {
+                urlBuilder.append("?");
+            }
+            urlBuilder.append("searchString=" + searchString + "&");
+        }
+        if (urlBuilder.toString().endsWith("&")) {
+            return urlBuilder.toString().substring(0, urlBuilder.toString().length()-1);
+        }
+        
+        return urlBuilder.toString();
+    }
+
+    /**
+     * Make product url according to the configurations.
+     * 
+     * @return String a catalog url
+     */
+    public static String makeProductUrl(String contextPath, List<String> trail, String productId, String productName, String currentCategoryId, String previousCategoryId) {
+        StringBuilder urlBuilder = new StringBuilder();
+        urlBuilder.append(contextPath);
+        if (urlBuilder.charAt(urlBuilder.length() - 1) != '/') {
+            urlBuilder.append("/");
+        }
+        if (!SeoConfigUtil.isCategoryUrlEnabled(contextPath)) {
+            urlBuilder.append(CatalogUrlServlet.CATALOG_URL_MOUNT_POINT);
+        } else {
+            urlBuilder.append(CatalogUrlServlet.PRODUCT_REQUEST + "/");
+        }
+
+        if (UtilValidate.isNotEmpty(currentCategoryId)) {
+            trail = CategoryWorker.adjustTrail(trail, currentCategoryId, previousCategoryId);
+            if (!SeoConfigUtil.isCategoryUrlEnabled(contextPath)) {
+                for (String trailCategoryId: trail) {
+                    if ("TOP".equals(trailCategoryId)) continue;
+                    urlBuilder.append("/");
+                    urlBuilder.append(trailCategoryId);
+                }
+            } else {
+                if (trail.size() > 1) {
+                    String lastCategoryId = trail.get(trail.size() - 1);
+                    if (!"TOP".equals(lastCategoryId)) {
+                        if (SeoConfigUtil.isCategoryNameEnabled()) {
+                            String categoryName = CatalogUrlSeoTransform.getCategoryIdNameMap().get(lastCategoryId);
+                            if (UtilValidate.isNotEmpty(categoryName)) {
+                                urlBuilder.append(categoryName + URL_HYPHEN);
+                            }
+                        }
+                    }
+                }
+            }
+        }
+
+        if (UtilValidate.isNotEmpty(productId)) {
+            if (!SeoConfigUtil.isCategoryUrlEnabled(contextPath)) {
+                urlBuilder.append("/p_");
+            } else {
+                productName = SeoUrlUtil.replaceSpecialCharsUrl(productName);
+                if (UtilValidate.isNotEmpty(productName)) {
+                    urlBuilder.append(productName + URL_HYPHEN);
+                }
+            }
+            urlBuilder.append(productId);
+        }
+        
+        if (!urlBuilder.toString().endsWith("/") && UtilValidate.isNotEmpty(SeoConfigUtil.getCategoryUrlSuffix())) {
+            urlBuilder.append(SeoConfigUtil.getCategoryUrlSuffix());
+        }
+        
+        return urlBuilder.toString();
+    }
+
+    /**
+     * Get a string lower cased and hyphen connected.
+     * 
+     * @param name a String to be transformed
+     * @return String nice name
+     */
+    protected static String getNiceName(String name) {
+        Perl5Matcher matcher = new Perl5Matcher();
+        String niceName = null;
+        if (UtilValidate.isNotEmpty(name)) {
+            name = name.trim().replaceAll(" ", URL_HYPHEN);
+            if (UtilValidate.isNotEmpty(name) && matcher.matches(name, asciiPattern)) {
+                niceName = name;
+            }
+        }
+        return niceName;
+    }
+    
+    public static boolean forwardProductUri(HttpServletRequest request, HttpServletResponse response, Delegator delegator) throws ServletException, IOException {
+        return forwardProductUri(request, response, delegator, null);
+    }
+
+    public static boolean forwardProductUri(HttpServletRequest request, HttpServletResponse response, Delegator delegator, String controlServlet) throws ServletException, IOException {
+        return forwardUri(request, response, delegator, controlServlet);
+    }
+
+    /**
+     * Forward a uri according to forward pattern regular expressions.
+     * 
+     * @param uri
+     *            String to reverse transform
+     * @return boolean to indicate whether the uri is forwarded.
+     * @throws IOException
+     * @throws ServletException
+     */
+    public static boolean forwardUri(HttpServletRequest request, HttpServletResponse response, Delegator delegator, String controlServlet) throws ServletException, IOException {
+        String pathInfo = request.getRequestURI();
+        String contextPath = request.getContextPath();
+        if (!isCategoryMapInitialed()) {
+            initCategoryMap(request, delegator);
+        }
+
+        if (!SeoConfigUtil.isCategoryUrlEnabled(contextPath)) {
+            return false;
+        }
+        List<String> pathElements = StringUtil.split(pathInfo, "/");
+        if (UtilValidate.isEmpty(pathElements)) {
+            return false;
+        }
+        // remove context path
+        pathInfo = SeoUrlUtil.removeContextPath(pathInfo, contextPath);
+        // remove servlet path
+        pathInfo = SeoUrlUtil.removeContextPath(pathInfo, request.getServletPath());
+        if (pathInfo.startsWith("/" + CatalogUrlServlet.CATEGORY_REQUEST + "/")) {
+            return forwardCategoryUri(request, response, delegator, controlServlet);
+        }
+        
+        String lastPathElement = pathElements.get(pathElements.size() - 1);
+        String categoryId = null;
+        String productId = null;
+        if (UtilValidate.isNotEmpty(lastPathElement)) {
+            if (UtilValidate.isNotEmpty(SeoConfigUtil.getCategoryUrlSuffix())) {
+                if (lastPathElement.endsWith(SeoConfigUtil.getCategoryUrlSuffix())) {
+                    lastPathElement = lastPathElement.substring(0, lastPathElement.length() - SeoConfigUtil.getCategoryUrlSuffix().length());
+                } else {
+                    return false;
+                }
+            }
+            if (SeoConfigUtil.isCategoryNameEnabled() || pathInfo.startsWith("/" + CatalogUrlServlet.CATEGORY_REQUEST + "/")) {
+                for (String categoryName : categoryNameIdMap.keySet()) {
+                    if (lastPathElement.startsWith(categoryName)) {
+                        categoryId = categoryNameIdMap.get(categoryName);
+                        if (!lastPathElement.equals(categoryName)) {
+                            lastPathElement = lastPathElement.substring(categoryName.length() + URL_HYPHEN.length());
+                        }
+                        break;
+                    }
+                }
+                if (UtilValidate.isEmpty(categoryId)) {
+                    categoryId = lastPathElement;
+                }
+            }
+
+            if (UtilValidate.isNotEmpty(lastPathElement)) {
+                List<String> urlElements = StringUtil.split(lastPathElement, URL_HYPHEN);
+                if (UtilValidate.isEmpty(urlElements)) {
+                    try {
+                        if (delegator.findOne("Product", UtilMisc.toMap("productId", lastPathElement), true) != null) {
+                            productId = lastPathElement;
+                        }
+                    } catch (GenericEntityException e) {
+                        Debug.logError(e, "Error looking up product info for ProductUrl with path info [" + pathInfo + "]: " + e.toString(), module);
+                    }
+                } else {
+                    int i = urlElements.size() - 1;
+                    String tempProductId = urlElements.get(i);
+                    while (i >= 0) {
+                        try {
+                            List<EntityExpr> exprs = FastList.newInstance();
+                            exprs.add(EntityCondition.makeCondition("productId", EntityOperator.EQUALS, lastPathElement));
+//                            if (SeoConfigUtil.isSpecialProductId(tempProductId)) {
+//                                exprs.add(EntityCondition.makeCondition("productId", EntityOperator.EQUALS, SeoConfigUtil.getSpecialProductId(tempProductId)));
+//                            } else {
+                                exprs.add(EntityCondition.makeCondition("productId", EntityOperator.EQUALS, tempProductId));
+//                            }
+                            List<GenericValue> products = delegator.findList("Product", EntityCondition.makeCondition(exprs, EntityOperator.OR), UtilMisc.toSet("productId", "productName"), null, null, true);
+                            
+                            if (products != null && products.size() > 0) {
+                                if (products.size() == 1) {
+                                    productId = products.get(0).getString("productId");
+                                    break;
+                                } else {
+                                    productId = tempProductId;
+                                    break;
+                                }
+                            } else if (i > 0) {
+                                tempProductId = urlElements.get(i - 1) + URL_HYPHEN + tempProductId;
+                            }
+                        } catch (GenericEntityException e) {
+                            Debug.logError(e, "Error looking up product info for ProductUrl with path info [" + pathInfo + "]: " + e.toString(), module);
+                        }
+                        i--;
+                    }
+                }
+            }
+        }
+
+        if (UtilValidate.isNotEmpty(productId) || UtilValidate.isNotEmpty(categoryId)) {
+            if (categoryId != null) {
+                request.setAttribute("productCategoryId", categoryId);
+            }
+
+            if (productId != null) {
+                request.setAttribute("product_id", productId);
+                request.setAttribute("productId", productId);
+            }
+
+            StringBuilder urlBuilder = new StringBuilder();
+            if (UtilValidate.isNotEmpty(controlServlet)) {
+                urlBuilder.append("/" + controlServlet);
+            }
+            urlBuilder.append("/" + (productId != null ? CatalogUrlServlet.PRODUCT_REQUEST : CatalogUrlServlet.CATEGORY_REQUEST));
+            UrlServletHelper.setViewQueryParameters(request, urlBuilder);
+            Debug.logInfo("[Filtered request]: " + pathInfo + " (" + urlBuilder + ")", module);
+            RequestDispatcher rd = request.getRequestDispatcher(urlBuilder.toString());
+            rd.forward(request, response);
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Forward a category uri according to forward pattern regular expressions.
+     * 
+     * @param uri
+     *            String to reverse transform
+     * @return String
+     * @throws IOException
+     * @throws ServletException
+     */
+    public static boolean forwardCategoryUri(HttpServletRequest request, HttpServletResponse response, Delegator delegator, String controlServlet) throws ServletException, IOException {
+        String pathInfo = request.getRequestURI();
+        String contextPath = request.getContextPath();
+        if (!isCategoryMapInitialed()) {
+            initCategoryMap(request);
+        }
+        if (!SeoConfigUtil.isCategoryUrlEnabled(contextPath)) {
+            return false;
+        }
+        List<String> pathElements = StringUtil.split(pathInfo, "/");
+        if (UtilValidate.isEmpty(pathElements)) {
+            return false;
+        }
+        String lastPathElement = pathElements.get(pathElements.size() - 1);
+        String categoryId = null;
+        if (UtilValidate.isNotEmpty(lastPathElement)) {
+            if (UtilValidate.isNotEmpty(SeoConfigUtil.getCategoryUrlSuffix())) {
+                if (lastPathElement.endsWith(SeoConfigUtil.getCategoryUrlSuffix())) {
+                    lastPathElement = lastPathElement.substring(0, lastPathElement.length() - SeoConfigUtil.getCategoryUrlSuffix().length());
+                } else {
+                    return false;
+                }
+            }
+            for (String categoryName : categoryNameIdMap.keySet()) {
+                if (lastPathElement.startsWith(categoryName)) {
+                    categoryId = categoryNameIdMap.get(categoryName);
+                    break;
+                }
+            }
+            if (UtilValidate.isEmpty(categoryId)) {
+                categoryId = lastPathElement.trim();
+            }
+        }
+        if (UtilValidate.isNotEmpty(categoryId)) {
+            request.setAttribute("productCategoryId", categoryId);
+            StringBuilder urlBuilder = new StringBuilder();
+            if (UtilValidate.isNotEmpty(controlServlet)) {
+                urlBuilder.append("/" + controlServlet);
+            }
+            urlBuilder.append("/" + CatalogUrlServlet.CATEGORY_REQUEST);
+            UrlServletHelper.setViewQueryParameters(request, urlBuilder);
+            Debug.logInfo("[Filtered request]: " + pathInfo + " (" + urlBuilder + ")", module);
+            RequestDispatcher rd = request.getRequestDispatcher(urlBuilder.toString());
+            rd.forward(request, response);
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * This is used when building product url in services.
+     * 
+     * @param delegator
+     * @param wrapper
+     * @param prefix
+     * @param contextPath
+     * @param productCategoryId
+     * @param previousCategoryId
+     * @param productId
+     * @return
+     */
+	public static String makeProductUrl(Delegator delegator, ProductContentWrapper wrapper, String prefix, String contextPath, String currentCategoryId, String previousCategoryId,
+			String productId) {
+        StringBuilder urlBuilder = new StringBuilder();
+        GenericValue product = null;
+        urlBuilder.append(prefix);
+        if (urlBuilder.charAt(urlBuilder.length() - 1) != '/') {
+            urlBuilder.append("/");
+        }
+        if (UtilValidate.isNotEmpty(productId)) {
+            try {
+                product = delegator.findOne("Product", UtilMisc.toMap("productId", productId), true);
+            } catch (GenericEntityException e) {
+                Debug.logError(e, "Error looking up product info for productId [" + productId + "]: " + e.toString(), module);
+            }
+        }
+        if (product != null) {
+            urlBuilder.append(CatalogUrlServlet.PRODUCT_REQUEST + "/");
+        }
+
+        if (UtilValidate.isNotEmpty(currentCategoryId)) {
+            List<String> trail = null;
+            trail = CategoryWorker.adjustTrail(trail, currentCategoryId, previousCategoryId);
+            if (!SeoConfigUtil.isCategoryUrlEnabled(contextPath)) {
+                for (String trailCategoryId: trail) {
+                    if ("TOP".equals(trailCategoryId)) continue;
+                    urlBuilder.append("/");
+                    urlBuilder.append(trailCategoryId);
+                }
+            } else {
+                if (trail != null && trail.size() > 1) {
+                    String lastCategoryId = trail.get(trail.size() - 1);
+                    if (!"TOP".equals(lastCategoryId)) {
+                        if (SeoConfigUtil.isCategoryNameEnabled()) {
+                            String categoryName = CatalogUrlSeoTransform.getCategoryIdNameMap().get(lastCategoryId);
+                            if (UtilValidate.isNotEmpty(categoryName)) {
+                                urlBuilder.append(categoryName);
+                                if (product != null) {
+                                    urlBuilder.append(URL_HYPHEN);
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        }
+
+        if (UtilValidate.isNotEmpty(productId)) {
+            if (product != null) {
+                String productName = product.getString("productName");
+                productName = SeoUrlUtil.replaceSpecialCharsUrl(productName);
+                if (UtilValidate.isNotEmpty(productName)) {
+                    urlBuilder.append(productName + URL_HYPHEN);
+                } else {
+                    StringWrapper alternativeUrl = wrapper.get("ALTERNATIVE_URL");
+                    if (UtilValidate.isNotEmpty(alternativeUrl) && UtilValidate.isNotEmpty(alternativeUrl.toString())) {
+                        productName = SeoUrlUtil.replaceSpecialCharsUrl(alternativeUrl.toString());
+                        if (UtilValidate.isNotEmpty(productName)) {
+                            urlBuilder.append(productName + URL_HYPHEN);
+                        }
+                    }
+                }
+            }
+            try {
+                //SeoConfigUtil.addSpecialProductId(productId);
+                urlBuilder.append(productId);
+            } catch (Exception e) {
+                urlBuilder.append(productId);
+            }
+        }
+        
+        if (!urlBuilder.toString().endsWith("/") && UtilValidate.isNotEmpty(SeoConfigUtil.getCategoryUrlSuffix())) {
+            urlBuilder.append(SeoConfigUtil.getCategoryUrlSuffix());
+        }
+        
+        return urlBuilder.toString();
+	}
+
+	/**
+     * This is used when building category url in services.
+	 * 
+	 * @param delegator
+	 * @param wrapper
+	 * @param prefix
+	 * @param productCategoryId
+	 * @param previousCategoryId
+	 * @param productId
+	 * @param viewSize
+	 * @param viewIndex
+	 * @param viewSort
+	 * @param searchString
+	 * @return
+	 */
+	public static String makeCategoryUrl(Delegator delegator, CategoryContentWrapper wrapper, String prefix,
+			String currentCategoryId, String previousCategoryId, String productId, String viewSize, String viewIndex,
+			String viewSort, String searchString) {
+        StringBuilder urlBuilder = new StringBuilder();
+        urlBuilder.append(prefix);
+        if (urlBuilder.charAt(urlBuilder.length() - 1) != '/') {
+            urlBuilder.append("/");
+        }
+        urlBuilder.append(CatalogUrlServlet.CATEGORY_REQUEST + "/");
+
+        if (UtilValidate.isNotEmpty(currentCategoryId)) {
+            List<String> trail = null;
+            trail = CategoryWorker.adjustTrail(trail, currentCategoryId, previousCategoryId);
+            if (trail != null && trail.size() > 1) {
+                String lastCategoryId = trail.get(trail.size() - 1);
+                if (!"TOP".equals(lastCategoryId)) {
+                    String categoryName = CatalogUrlSeoTransform.getCategoryIdNameMap().get(lastCategoryId);
+                    if (UtilValidate.isNotEmpty(categoryName)) {
+                        urlBuilder.append(categoryName);
+                        urlBuilder.append(URL_HYPHEN);
+                        urlBuilder.append(lastCategoryId.trim().replaceAll(" ", URL_HYPHEN));
+                    } else {
+                        urlBuilder.append(lastCategoryId.trim().replaceAll(" ", URL_HYPHEN));
+                    }
+                }
+            }
+        }
+
+        if (!urlBuilder.toString().endsWith("/") && UtilValidate.isNotEmpty(SeoConfigUtil.getCategoryUrlSuffix())) {
+            urlBuilder.append(SeoConfigUtil.getCategoryUrlSuffix());
+        }
+        
+        // append view index
+        if (UtilValidate.isNotEmpty(viewIndex)) {
+            if (!urlBuilder.toString().endsWith("?") && !urlBuilder.toString().endsWith("&")) {
+                urlBuilder.append("?");
+            }
+            urlBuilder.append("viewIndex=" + viewIndex + "&");
+        }
+        // append view size
+        if (UtilValidate.isNotEmpty(viewSize)) {
+            if (!urlBuilder.toString().endsWith("?") && !urlBuilder.toString().endsWith("&")) {
+                urlBuilder.append("?");
+            }
+            urlBuilder.append("viewSize=" + viewSize + "&");
+        }
+        // append view sort
+        if (UtilValidate.isNotEmpty(viewSort)) {
+            if (!urlBuilder.toString().endsWith("?") && !urlBuilder.toString().endsWith("&")) {
+                urlBuilder.append("?");
+            }
+            urlBuilder.append("viewSort=" + viewSort + "&");
+        }
+        // append search string
+        if (UtilValidate.isNotEmpty(searchString)) {
+            if (!urlBuilder.toString().endsWith("?") && !urlBuilder.toString().endsWith("&")) {
+                urlBuilder.append("?");
+            }
+            urlBuilder.append("searchString=" + searchString + "&");
+        }
+        if (urlBuilder.toString().endsWith("&")) {
+            return urlBuilder.toString().substring(0, urlBuilder.toString().length()-1);
+        }
+        
+        return urlBuilder.toString();
+	}
+}
diff --git a/applications/product/src/org/ofbiz/product/category/OfbizCatalogAltUrlTransform.java b/applications/product/src/org/ofbiz/product/category/ftl/OfbizCatalogAltUrlTransform.java
similarity index 89%
rename from applications/product/src/org/ofbiz/product/category/OfbizCatalogAltUrlTransform.java
rename to applications/product/src/org/ofbiz/product/category/ftl/OfbizCatalogAltUrlTransform.java
index f8f6373..3081381 100644
--- a/applications/product/src/org/ofbiz/product/category/OfbizCatalogAltUrlTransform.java
+++ b/applications/product/src/org/ofbiz/product/category/ftl/OfbizCatalogAltUrlTransform.java
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  *******************************************************************************/
-package org.ofbiz.product.category;
+package org.ofbiz.product.category.ftl;
 
 import java.io.IOException;
 import java.io.Writer;
@@ -25,13 +25,15 @@
 
 import javax.servlet.http.HttpServletRequest;
 
+import org.ofbiz.base.util.Debug;
 import org.ofbiz.base.util.UtilMisc;
 import org.ofbiz.base.util.UtilValidate;
 import org.ofbiz.base.util.template.FreeMarkerWorker;
 import org.ofbiz.entity.Delegator;
 import org.ofbiz.entity.GenericEntityException;
 import org.ofbiz.entity.GenericValue;
-import org.ofbiz.entity.util.EntityQuery;
+import org.ofbiz.product.category.CatalogUrlFilter;
+import org.ofbiz.product.category.CategoryContentWrapper;
 import org.ofbiz.product.product.ProductContentWrapper;
 import org.ofbiz.service.LocalDispatcher;
 import org.ofbiz.webapp.OfbizUrlBuilder;
@@ -49,7 +51,6 @@
 public class OfbizCatalogAltUrlTransform implements TemplateTransformModel {
     public final static String module = OfbizCatalogUrlTransform.class.getName();
 
-    @SuppressWarnings("unchecked")
     public String getStringArg(Map args, String key) {
         Object o = args.get(key);
         if (o instanceof SimpleScalar) {
@@ -78,7 +79,6 @@
     }
 
     @Override
-    @SuppressWarnings("unchecked")
     public Writer getWriter(final Writer out, final Map args)
             throws TemplateModelException, IOException {
         final StringBuilder buf = new StringBuilder();
@@ -122,8 +122,12 @@
                         }
                         // make the link
                         if (fullPath){
-                            OfbizUrlBuilder builder = OfbizUrlBuilder.from(request);
-                            builder.buildHostPart(newURL, url, secure);
+                            try {
+                                OfbizUrlBuilder builder = OfbizUrlBuilder.from(request);
+                                builder.buildHostPart(newURL, "", secure);
+                            } catch (WebAppConfigurationException e) {
+                                Debug.logError(e.getMessage(), module);
+                            }
                         }
                         newURL.append(url);
                         out.write(newURL.toString());
@@ -132,11 +136,11 @@
                         LocalDispatcher dispatcher = FreeMarkerWorker.getWrappedObject("dispatcher", env);
                         Locale locale = (Locale) args.get("locale");
                         if (UtilValidate.isNotEmpty(productId)) {
-                            GenericValue product = EntityQuery.use(delegator).from("Product").where("productId", productId).queryOne();
+                            GenericValue product = delegator.findOne("Product", UtilMisc.toMap("productId", productId), false);
                             ProductContentWrapper wrapper = new ProductContentWrapper(dispatcher, product, locale, "text/html");
                             url = CatalogUrlFilter.makeProductUrl(delegator, wrapper, null, ((StringModel) prefix).getAsString(), previousCategoryId, productCategoryId, productId);
                         } else {
-                            GenericValue productCategory = EntityQuery.use(delegator).from("ProductCategory").where("productCategoryId", productCategoryId).queryOne();
+                            GenericValue productCategory = delegator.findOne("ProductCategory", UtilMisc.toMap("productCategoryId", productCategoryId), false);
                             CategoryContentWrapper wrapper = new CategoryContentWrapper(dispatcher, productCategory, locale, "text/html");
                             url = CatalogUrlFilter.makeCategoryUrl(delegator, wrapper, null, ((StringModel) prefix).getAsString(), previousCategoryId, productCategoryId, productId, viewSize, viewIndex, viewSort, searchString);
                         }
@@ -148,8 +152,6 @@
                     throw new IOException(e.getMessage());
                 } catch (GenericEntityException e) {
                     throw new IOException(e.getMessage());
-                } catch (WebAppConfigurationException e) {
-                    throw new IOException(e.getMessage());
                 }
             }
         };
diff --git a/applications/product/src/org/ofbiz/product/category/OfbizCatalogUrlTransform.java b/applications/product/src/org/ofbiz/product/category/ftl/OfbizCatalogUrlTransform.java
similarity index 96%
rename from applications/product/src/org/ofbiz/product/category/OfbizCatalogUrlTransform.java
rename to applications/product/src/org/ofbiz/product/category/ftl/OfbizCatalogUrlTransform.java
index bd9c451..a51708e 100644
--- a/applications/product/src/org/ofbiz/product/category/OfbizCatalogUrlTransform.java
+++ b/applications/product/src/org/ofbiz/product/category/ftl/OfbizCatalogUrlTransform.java
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  *******************************************************************************/
-package org.ofbiz.product.category;
+package org.ofbiz.product.category.ftl;
 
 import java.io.IOException;
 import java.io.Writer;
@@ -24,6 +24,8 @@
 
 import javax.servlet.http.HttpServletRequest;
 
+import org.ofbiz.product.category.CatalogUrlServlet;
+
 import freemarker.core.Environment;
 import freemarker.ext.beans.BeanModel;
 import freemarker.ext.beans.StringModel;
@@ -34,7 +36,6 @@
 public class OfbizCatalogUrlTransform implements TemplateTransformModel {
     public final static String module = OfbizCatalogUrlTransform.class.getName();
     
-    @SuppressWarnings("unchecked")
     public String getStringArg(Map args, String key) {
         Object o = args.get(key);
         if (o instanceof SimpleScalar) {
@@ -46,7 +47,6 @@
     }
 
     @Override
-    @SuppressWarnings("unchecked")
     public Writer getWriter(final Writer out, final Map args) throws TemplateModelException, IOException {
         final StringBuilder buf = new StringBuilder();
         return new Writer(out) {
diff --git a/applications/product/src/org/ofbiz/product/category/ftl/SeoTransform.java b/applications/product/src/org/ofbiz/product/category/ftl/SeoTransform.java
new file mode 100644
index 0000000..1f78087
--- /dev/null
+++ b/applications/product/src/org/ofbiz/product/category/ftl/SeoTransform.java
@@ -0,0 +1,189 @@
+/*******************************************************************************
+ * 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.
+ *******************************************************************************/
+package org.ofbiz.product.category.ftl;
+
+import java.io.IOException;
+import java.io.Writer;
+import java.util.Iterator;
+import java.util.Map;
+
+import javax.servlet.ServletContext;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
+
+import org.apache.oro.text.regex.Pattern;
+import org.apache.oro.text.regex.Perl5Matcher;
+import org.ofbiz.base.util.Debug;
+import org.ofbiz.entity.GenericValue;
+import org.ofbiz.product.category.SeoConfigUtil;
+import org.ofbiz.webapp.control.RequestHandler;
+
+import freemarker.core.Environment;
+import freemarker.ext.beans.BeanModel;
+import freemarker.template.SimpleScalar;
+import freemarker.template.TemplateScalarModel;
+import freemarker.template.TemplateTransformModel;
+
+/**
+ * SeoTransform - Freemarker Transform for URLs (links)
+ * 
+ */
+public class SeoTransform implements TemplateTransformModel {
+
+    private static final String module = SeoTransform.class.getName();
+
+    public boolean checkArg(Map args, String key, boolean defaultValue) {
+        if (!args.containsKey(key)) {
+            return defaultValue;
+        } else {
+            Object o = args.get(key);
+            if (o instanceof SimpleScalar) {
+                SimpleScalar s = (SimpleScalar) o;
+                return "true".equalsIgnoreCase(s.getAsString());
+            }
+            return defaultValue;
+        }
+    }
+
+    public Writer getWriter(final Writer out, Map args) {
+        final StringBuffer buf = new StringBuffer();
+        final boolean fullPath = checkArg(args, "fullPath", false);
+        final boolean secure = checkArg(args, "secure", false);
+        final boolean encode = checkArg(args, "encode", true);
+
+        return new Writer(out) {
+
+            public void write(char cbuf[], int off, int len) {
+                buf.append(cbuf, off, len);
+            }
+
+            public void flush() throws IOException {
+                out.flush();
+            }
+
+            public void close() throws IOException {
+                try {
+                    Environment env = Environment.getCurrentEnvironment();
+                    BeanModel req = (BeanModel) env.getVariable("request");
+                    BeanModel res = (BeanModel) env.getVariable("response");
+                    Object prefix = env.getVariable("urlPrefix");
+                    if (req != null) {
+                        HttpServletRequest request = (HttpServletRequest) req.getWrappedObject();
+                        ServletContext ctx = (ServletContext) request.getAttribute("servletContext");
+                        HttpServletResponse response = null;
+                        if (res != null) {
+                            response = (HttpServletResponse) res.getWrappedObject();
+                        }
+                        HttpSession session = request.getSession();
+                        GenericValue userLogin = (GenericValue) session.getAttribute("userLogin");
+
+                        // anonymous shoppers are not logged in
+                        if (userLogin != null && "anonymous".equals(userLogin.getString("userLoginId"))) {
+                            userLogin = null;
+                        }
+
+                        RequestHandler rh = (RequestHandler) ctx.getAttribute("_REQUEST_HANDLER_");
+                        out.write(seoUrl(rh.makeLink(request, response, buf.toString(), fullPath, secure, encode), userLogin == null));
+                    } else if (prefix != null) {
+                        if (prefix instanceof TemplateScalarModel) {
+                            TemplateScalarModel s = (TemplateScalarModel) prefix;
+                            String prefixString = s.getAsString();
+                            String bufString = buf.toString();
+                            boolean prefixSlash = prefixString.endsWith("/");
+                            boolean bufSlash = bufString.startsWith("/");
+                            if (prefixSlash && bufSlash) {
+                                bufString = bufString.substring(1);
+                            } else if (!prefixSlash && !bufSlash) {
+                                bufString = "/" + bufString;
+                            }
+                            out.write(prefixString + bufString);
+                        }
+                    } else {
+                        out.write(buf.toString());
+                    }
+                } catch (Exception e) {
+                    throw new IOException(e.getMessage());
+                }
+            }
+        };
+    }
+
+    /**
+     * Transform a url according to seo pattern regular expressions.
+     * 
+     * @param url , String to do the seo transform
+     * @param isAnon , boolean to indicate whether it's an anonymous visit.
+     * 
+     * @return String, the transformed url.
+     */
+    public static String seoUrl(String url, boolean isAnon) {
+        Perl5Matcher matcher = new Perl5Matcher();
+        if (SeoConfigUtil.checkUseUrlRegexp() && matcher.matches(url, SeoConfigUtil.getGeneralRegexpPattern())) {
+            Iterator<String> keys = SeoConfigUtil.getSeoPatterns().keySet().iterator();
+            boolean foundMatch = false;
+            while (keys.hasNext()) {
+                String key = keys.next();
+                Pattern pattern = SeoConfigUtil.getSeoPatterns().get(key);
+                if (pattern.getPattern().contains(";jsessionid=")) {
+                    if (isAnon) {
+                        if (SeoConfigUtil.isJSessionIdAnonEnabled()) {
+                            continue;
+                        }
+                    } else {
+                        if (SeoConfigUtil.isJSessionIdUserEnabled()) {
+                            continue;
+                        } else {
+                            boolean foundException = false;
+                            for (int i = 0; i < SeoConfigUtil.getUserExceptionPatterns().size(); i++) {
+                                if (matcher.matches(url, SeoConfigUtil.getUserExceptionPatterns().get(i))) {
+                                    foundException = true;
+                                    break;
+                                }
+                            }
+                            if (foundException) {
+                                continue;
+                            }
+                        }
+                    }
+                }
+                String replacement = SeoConfigUtil.getSeoReplacements().get(key);
+                if (matcher.matches(url, pattern)) {
+                    for (int i = 1; i < matcher.getMatch().groups(); i++) {
+                        replacement = replacement.replaceAll("\\$" + i, matcher.getMatch().group(i));
+                    }
+                    // break if found any matcher
+                    url = replacement;
+                    foundMatch = true;
+                    break;
+                }
+            }
+            if (!foundMatch) {
+                Debug.logVerbose("Can NOT find a seo transform pattern for this url: " + url, module);
+            }
+        }
+        return url;
+    }
+
+    static {
+        if (!SeoConfigUtil.isInitialed()) {
+            SeoConfigUtil.init();
+        }
+    }
+}
diff --git a/applications/product/src/org/ofbiz/product/category/ftl/UrlRegexpTransform.java b/applications/product/src/org/ofbiz/product/category/ftl/UrlRegexpTransform.java
new file mode 100644
index 0000000..58f5547
--- /dev/null
+++ b/applications/product/src/org/ofbiz/product/category/ftl/UrlRegexpTransform.java
@@ -0,0 +1,231 @@
+/*******************************************************************************
+ * 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.
+ *******************************************************************************/
+package org.ofbiz.product.category.ftl;
+
+import java.io.IOException;
+import java.io.Writer;
+import java.util.Iterator;
+import java.util.Map;
+
+import javax.servlet.ServletContext;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
+
+import org.apache.oro.text.regex.Pattern;
+import org.apache.oro.text.regex.Perl5Matcher;
+import org.ofbiz.base.util.Debug;
+import org.ofbiz.entity.GenericValue;
+import org.ofbiz.product.category.SeoConfigUtil;
+import org.ofbiz.webapp.control.RequestHandler;
+
+import freemarker.core.Environment;
+import freemarker.ext.beans.BeanModel;
+import freemarker.template.SimpleScalar;
+import freemarker.template.TemplateScalarModel;
+import freemarker.template.TemplateTransformModel;
+
+/**
+ * UrlRegexpTransform - Freemarker Transform for Products URLs (links)
+ * 
+ */
+public class UrlRegexpTransform implements TemplateTransformModel {
+
+    private static final String module = UrlRegexpTransform.class.getName();
+
+    public boolean checkArg(Map args, String key, boolean defaultValue) {
+        if (!args.containsKey(key)) {
+            return defaultValue;
+        } else {
+            Object o = args.get(key);
+            if (o instanceof SimpleScalar) {
+                SimpleScalar s = (SimpleScalar) o;
+                return "true".equalsIgnoreCase(s.getAsString());
+            }
+            return defaultValue;
+        }
+    }
+
+    public Writer getWriter(final Writer out, Map args) {
+        final StringBuffer buf = new StringBuffer();
+        final boolean fullPath = checkArg(args, "fullPath", false);
+        final boolean secure = checkArg(args, "secure", false);
+        final boolean encode = checkArg(args, "encode", true);
+
+        return new Writer(out) {
+
+            public void write(char cbuf[], int off, int len) {
+                buf.append(cbuf, off, len);
+            }
+
+            public void flush() throws IOException {
+                out.flush();
+            }
+
+            public void close() throws IOException {
+                try {
+                    Environment env = Environment.getCurrentEnvironment();
+                    BeanModel req = (BeanModel) env.getVariable("request");
+                    BeanModel res = (BeanModel) env.getVariable("response");
+                    Object prefix = env.getVariable("urlPrefix");
+                    if (req != null) {
+                        HttpServletRequest request = (HttpServletRequest) req.getWrappedObject();
+                        ServletContext ctx = (ServletContext) request.getAttribute("servletContext");
+                        HttpServletResponse response = null;
+                        if (res != null) {
+                            response = (HttpServletResponse) res.getWrappedObject();
+                        }
+                        HttpSession session = request.getSession();
+                        GenericValue userLogin = (GenericValue) session.getAttribute("userLogin");
+
+                        // anonymous shoppers are not logged in
+                        if (userLogin != null && "anonymous".equals(userLogin.getString("userLoginId"))) {
+                            userLogin = null;
+                        }
+
+                        RequestHandler rh = (RequestHandler) ctx.getAttribute("_REQUEST_HANDLER_");
+                        out.write(seoUrl(rh.makeLink(request, response, buf.toString(), fullPath, secure, encode), userLogin == null));
+                    } else if (prefix != null) {
+                        if (prefix instanceof TemplateScalarModel) {
+                            TemplateScalarModel s = (TemplateScalarModel) prefix;
+                            String prefixString = s.getAsString();
+                            String bufString = buf.toString();
+                            boolean prefixSlash = prefixString.endsWith("/");
+                            boolean bufSlash = bufString.startsWith("/");
+                            if (prefixSlash && bufSlash) {
+                                bufString = bufString.substring(1);
+                            } else if (!prefixSlash && !bufSlash) {
+                                bufString = "/" + bufString;
+                            }
+                            out.write(prefixString + bufString);
+                        }
+                    } else {
+                        out.write(buf.toString());
+                    }
+                } catch (Exception e) {
+                    throw new IOException(e.getMessage());
+                }
+            }
+        };
+    }
+
+    /**
+     * Transform a url according to seo pattern regular expressions.
+     * 
+     * @param url
+     *            , String to do the seo transform
+     * @param isAnon
+     *            , boolean to indicate whether it's an anonymous visit.
+     * 
+     * @return String, the transformed url.
+     */
+    public static String seoUrl(String url, boolean isAnon) {
+        Perl5Matcher matcher = new Perl5Matcher();
+        if (SeoConfigUtil.checkUseUrlRegexp() && matcher.matches(url, SeoConfigUtil.getGeneralRegexpPattern())) {
+            Iterator<String> keys = SeoConfigUtil.getSeoPatterns().keySet().iterator();
+            boolean foundMatch = false;
+            while (keys.hasNext()) {
+                String key = keys.next();
+                Pattern pattern = SeoConfigUtil.getSeoPatterns().get(key);
+                if (pattern.getPattern().contains(";jsessionid=")) {
+                    if (isAnon) {
+                        if (SeoConfigUtil.isJSessionIdAnonEnabled()) {
+                            continue;
+                        }
+                    } else {
+                        if (SeoConfigUtil.isJSessionIdUserEnabled()) {
+                            continue;
+                        } else {
+                            boolean foundException = false;
+                            for (int i = 0; i < SeoConfigUtil.getUserExceptionPatterns().size(); i++) {
+                                if (matcher.matches(url, SeoConfigUtil.getUserExceptionPatterns().get(i))) {
+                                    foundException = true;
+                                    break;
+                                }
+                            }
+                            if (foundException) {
+                                continue;
+                            }
+                        }
+                    }
+                }
+                String replacement = SeoConfigUtil.getSeoReplacements().get(key);
+                if (matcher.matches(url, pattern)) {
+                    for (int i = 1; i < matcher.getMatch().groups(); i++) {
+                        replacement = replacement.replaceAll("\\$" + i, matcher.getMatch().group(i));
+                    }
+                    // break if found any matcher
+                    url = replacement;
+                    foundMatch = true;
+                    break;
+                }
+            }
+            if (!foundMatch) {
+                Debug.logVerbose("Can NOT find a seo transform pattern for this url: " + url, module);
+            }
+        }
+        return url;
+    }
+
+    static {
+        SeoConfigUtil.init();
+    }
+
+    /**
+     * Forward a uri according to forward pattern regular expressions. Note: this is developed for Filter usage.
+     * 
+     * @param uri
+     *            String to reverse transform
+     * @return String
+     */
+    public static boolean forwardUri(HttpServletResponse response, String uri) {
+        Perl5Matcher matcher = new Perl5Matcher();
+        boolean foundMatch = false;
+        Integer responseCodeInt = null;
+        if (SeoConfigUtil.checkUseUrlRegexp() && SeoConfigUtil.getSeoPatterns() != null && SeoConfigUtil.getForwardReplacements() != null) {
+            Iterator<String> keys = SeoConfigUtil.getSeoPatterns().keySet().iterator();
+            while (keys.hasNext()) {
+                String key = keys.next();
+                Pattern pattern = SeoConfigUtil.getSeoPatterns().get(key);
+                String replacement = SeoConfigUtil.getForwardReplacements().get(key);
+                if (matcher.matches(uri, pattern)) {
+                    for (int i = 1; i < matcher.getMatch().groups(); i++) {
+                        replacement = replacement.replaceAll("\\$" + i, matcher.getMatch().group(i));
+                    }
+                    // break if found any matcher
+                    uri = replacement;
+                    responseCodeInt = SeoConfigUtil.getForwardResponseCodes().get(key);
+                    foundMatch = true;
+                    break;
+                }
+            }
+        }
+        if (foundMatch) {
+            if (responseCodeInt == null) {
+                response.setStatus(SeoConfigUtil.DEFAULT_RESPONSECODE);
+            } else {
+                response.setStatus(responseCodeInt.intValue());
+            }
+            response.setHeader("Location", uri);
+        } else {
+            Debug.logInfo("Can NOT forward this url: " + uri, module);
+        }
+        return foundMatch;
+    }
+}
diff --git a/applications/product/widget/catalog/CatalogMenus.xml b/applications/product/widget/catalog/CatalogMenus.xml
index 545de0d..3e144f4 100644
--- a/applications/product/widget/catalog/CatalogMenus.xml
+++ b/applications/product/widget/catalog/CatalogMenus.xml
@@ -532,9 +532,7 @@
             <condition>
                 <not><if-empty field="product"/></not>
             </condition>
-            <link target="/ecommerce/control/product" url-mode="inter-app">
-                <parameter param-name="product_id" from-field="productId"/>
-            </link>
+            <link target="/ecommerce/product/${ecomLink}" url-mode="inter-app" />
         </menu-item>
         <menu-item name="ProductBarCode" title="${uiLabelMap.ProductBarcode}">
             <condition>
diff --git a/applications/product/widget/catalog/ProductScreens.xml b/applications/product/widget/catalog/ProductScreens.xml
index 7e6b63f..b6f6e89 100644
--- a/applications/product/widget/catalog/ProductScreens.xml
+++ b/applications/product/widget/catalog/ProductScreens.xml
@@ -33,6 +33,8 @@
                 <set field="productId" from-field="parameters.productId" global="true"/>
                 <entity-one entity-name="Product" value-field="product"/>
                 <set field="product" from-field="product" global="true"/>
+                <set field="ecomLink" value="${groovy: product.internalName.replace(' ', org.ofbiz.product.category.ftl.CatalogUrlSeoTransform.URL_HYPHEN) 
+                    + org.ofbiz.product.category.ftl.CatalogUrlSeoTransform.URL_HYPHEN + productId + '.html'}"/>                
             </actions>
             <widgets>
                 <decorator-screen name="main-decorator" location="${parameters.mainDecoratorLocation}">
diff --git a/framework/webapp/config/freemarkerTransforms.properties b/framework/webapp/config/freemarkerTransforms.properties
index ae3a32a..3866359 100644
--- a/framework/webapp/config/freemarkerTransforms.properties
+++ b/framework/webapp/config/freemarkerTransforms.properties
@@ -21,7 +21,8 @@
 
 # entries are in the form: key=transform name, property=transform class name
 
-ofbizUrl=org.ofbiz.webapp.ftl.OfbizUrlTransform
+#ofbizUrl=org.ofbiz.webapp.ftl.OfbizUrlTransform
+ofbizUrl=org.ofbiz.product.category.ftl.UrlRegexpTransform
 ofbizContentUrl=org.ofbiz.webapp.ftl.OfbizContentTransform
 ofbizCurrency=org.ofbiz.webapp.ftl.OfbizCurrencyTransform
 ofbizAmount=org.ofbiz.webapp.ftl.OfbizAmountTransform
diff --git a/framework/webapp/src/org/ofbiz/webapp/WebAppUtil.java b/framework/webapp/src/org/ofbiz/webapp/WebAppUtil.java
index 0e0adce..cc62d08 100644
--- a/framework/webapp/src/org/ofbiz/webapp/WebAppUtil.java
+++ b/framework/webapp/src/org/ofbiz/webapp/WebAppUtil.java
@@ -66,7 +66,7 @@
         String servletMapping = null;
         WebXml webXml = getWebXml(webAppInfo);
         for (ServletDef servletDef : webXml.getServlets().values()) {
-            if ("org.ofbiz.webapp.control.ControlServlet".equals(servletDef.getServletClass())) {
+            if ("org.ofbiz.webapp.control.ControlServlet".equals(servletDef.getServletClass()) || "org.ofbiz.product.category.SeoControlServlet".equals(servletDef.getServletClass())) {
                 String servletName = servletDef.getServletName();
                 // Catalina servlet mappings: key = url-pattern, value = servlet-name.
                 for (Entry<String, String> entry : webXml.getServletMappings().entrySet()) {
diff --git a/framework/webapp/src/org/ofbiz/webapp/control/RequestHandler.java b/framework/webapp/src/org/ofbiz/webapp/control/RequestHandler.java
index ad02bdc..6e8546c 100644
--- a/framework/webapp/src/org/ofbiz/webapp/control/RequestHandler.java
+++ b/framework/webapp/src/org/ofbiz/webapp/control/RequestHandler.java
@@ -862,7 +862,7 @@
     }
     private void renderView(String view, boolean allowExtView, HttpServletRequest req, HttpServletResponse resp, String saveName) throws RequestHandlerException {
         GenericValue userLogin = (GenericValue) req.getSession().getAttribute("userLogin");
-        // workaraound if we are in the root webapp
+        // workaround if we are in the root webapp
         String cname = UtilHttp.getApplicationName(req);
         String oldView = view;
 
@@ -873,12 +873,11 @@
         // if the view name starts with the control servlet name and a /, then it was an
         // attempt to override the default view with a call back into the control servlet,
         // so just get the target view name and use that
-        
         String servletName = req.getServletPath();
-        if (servletName.startsWith("/")) {
+        if (UtilValidate.isNotEmpty(servletName) && servletName.length() > 1 || servletName.startsWith("/")) {
             servletName = servletName.substring(1);
         }
-
+        
         if (Debug.infoOn()) Debug.logInfo("Rendering View [" + view + "], sessionId=" + UtilHttp.getSessionId(req), module);
         if (view.startsWith(servletName + "/")) {
             view = view.substring(servletName.length() + 1);
diff --git a/specialpurpose/ecommerce/src/org/ofbiz/ecommerce/webapp/view/JspViewHandler.java b/specialpurpose/ecommerce/src/org/ofbiz/ecommerce/webapp/view/JspViewHandler.java
new file mode 100644
index 0000000..e4cb1f9
--- /dev/null
+++ b/specialpurpose/ecommerce/src/org/ofbiz/ecommerce/webapp/view/JspViewHandler.java
@@ -0,0 +1,103 @@
+/*******************************************************************************
+ * 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.
+ *******************************************************************************/
+package org.ofbiz.ecommerce.webapp.view;
+
+import java.io.IOException;
+import java.net.URLEncoder;
+
+import javax.servlet.RequestDispatcher;
+import javax.servlet.ServletContext;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.jsp.JspException;
+
+import org.ofbiz.base.util.Debug;
+import org.ofbiz.base.util.UtilValidate;
+import org.ofbiz.webapp.control.ContextFilter;
+import org.ofbiz.webapp.view.AbstractViewHandler;
+import org.ofbiz.webapp.view.ViewHandlerException;
+
+/**
+ * JspViewHandler - Java Server Pages View Handler
+ */
+public class JspViewHandler extends AbstractViewHandler {
+
+    public static final String module = JspViewHandler.class.getName();
+
+    protected ServletContext context;
+
+    public void init(ServletContext context) throws ViewHandlerException {
+        this.context = context;
+    }
+
+    public void render(String name, String page, String contentType, String encoding, String info, HttpServletRequest request, HttpServletResponse response) throws ViewHandlerException {
+        // some containers call filters on EVERY request, even forwarded ones,
+        // so let it know that it came from the control servlet
+
+        if (request == null) {
+            throw new ViewHandlerException("Null HttpServletRequest object");
+        }
+        if (UtilValidate.isEmpty(page)) {
+            throw new ViewHandlerException("Null or empty source");
+        }
+
+        // Debug.logInfo("Requested Page : " + page, module);
+        // Debug.logInfo("Physical Path  : " + context.getRealPath(page));
+
+        // tell the ContextFilter we are forwarding
+        request.setAttribute(ContextFilter.FORWARDED_FROM_SERVLET, Boolean.TRUE);
+        RequestDispatcher rd = request.getRequestDispatcher(page);
+
+        if (rd == null) {
+            Debug.logInfo("HttpServletRequest.getRequestDispatcher() failed; trying ServletContext", module);
+            rd = context.getRequestDispatcher(page);
+            if (rd == null) {
+                Debug.logInfo("ServletContext.getRequestDispatcher() failed; trying ServletContext.getNamedDispatcher(\"jsp\")", module);
+                rd = context.getNamedDispatcher("jsp");
+                if (rd == null) {
+                    throw new ViewHandlerException("Source returned a null dispatcher (" + page + ")");
+                }
+            }
+        }
+
+        try {
+        	if (UtilValidate.isEmpty(request.getServletPath())) {
+        		// no context or filter to service this page, so we have to forward it directly and let SeoControlServlet to resolve it
+        		String uri = URLEncoder.encode(request.getContextPath() + page, "UTF-8");
+        		request.setAttribute("_jsp_" + uri, Boolean.TRUE);
+        		rd.forward(request, response);
+        	} else {
+        		rd.include(request, response);
+        	}
+        } catch (IOException ie) {
+            throw new ViewHandlerException("IO Error in view", ie);
+        } catch (ServletException e) {
+            Throwable throwable = e.getRootCause() != null ? e.getRootCause() : e;
+
+            if (throwable instanceof JspException) {
+                JspException jspe = (JspException) throwable;
+
+                throwable = jspe.getCause() != null ? jspe.getCause() : jspe;
+            }
+            Debug.logError(throwable, "ServletException rendering JSP view", module);
+            throw new ViewHandlerException(e.getMessage(), throwable);
+        }
+    }
+}
diff --git a/specialpurpose/ecommerce/webapp/ecommerce/WEB-INF/controller.xml b/specialpurpose/ecommerce/webapp/ecommerce/WEB-INF/controller.xml
index 7819d50..a938312 100644
--- a/specialpurpose/ecommerce/webapp/ecommerce/WEB-INF/controller.xml
+++ b/specialpurpose/ecommerce/webapp/ecommerce/WEB-INF/controller.xml
@@ -31,7 +31,7 @@
     <handler name="simple" type="request" class="org.ofbiz.webapp.event.SimpleEventHandler"/>
     <handler name="rome" type="request" class="org.ofbiz.webapp.event.RomeEventHandler"/>
 
-    <handler name="jsp" type="view" class="org.ofbiz.webapp.view.JspViewHandler"/>
+    <handler name="jsp" type="view" class="org.ofbiz.ecommerce.webapp.view.JspViewHandler"/>
     <handler name="http" type="view" class="org.ofbiz.webapp.view.HttpViewHandler"/>
     <handler name="screen" type="view" class="org.ofbiz.widget.screen.MacroScreenViewHandler"/>
     <handler name="simplecontent" type="view" class="org.ofbiz.content.view.SimpleContentViewHandler"/>
@@ -2020,7 +2020,7 @@
     <!-- End of Request Mappings -->
 
     <!-- View Mappings -->
-    <view-map name="error" page="/error/error.jsp"/>
+    <view-map name="error" type="jsp" page="/error/error.jsp"/>
     <view-map name="main" type="screen" page="component://ecommerce/widget/CommonScreens.xml#main"/>
     <view-map name="policies" type="screen" page="component://ecommerce/widget/CommonScreens.xml#policies"/>
     <view-map name="login" type="screen" page="component://ecommerce/widget/CommonScreens.xml#login"/>
diff --git a/specialpurpose/ecommerce/webapp/ecommerce/WEB-INF/web.xml b/specialpurpose/ecommerce/webapp/ecommerce/WEB-INF/web.xml
index da3b16d..ba92866 100644
--- a/specialpurpose/ecommerce/webapp/ecommerce/WEB-INF/web.xml
+++ b/specialpurpose/ecommerce/webapp/ecommerce/WEB-INF/web.xml
@@ -52,17 +52,23 @@
             frequently switch between http and https.
         </description>
     </context-param>
+    <context-param>
+        <param-name>defaultPage</param-name>
+        <param-value>/main</param-value>
+        <description>Default page uri. Important: please DO add or remove /control to match url-pattern of SeoControlServlet.
+        </description>
+    </context-param>
 
     <filter>
-        <filter-name>CatalogUrlFilter</filter-name>
-        <display-name>CatalogUrlFilter</display-name>
-        <filter-class>org.ofbiz.product.category.CatalogUrlFilter</filter-class>
+        <filter-name>SeoCatalogUrlFilter</filter-name>
+        <display-name>SeoCatalogUrlFilter</display-name>
+        <filter-class>org.ofbiz.product.category.CatalogUrlSeoFilter</filter-class>
         <init-param><param-name>defaultLocaleString</param-name><param-value>en_US</param-value></init-param>
     </filter>
     <filter>
-        <filter-name>ContentUrlFilter</filter-name>
-        <display-name>ContentUrlFilter</display-name>
-        <filter-class>org.ofbiz.content.content.ContentUrlFilter</filter-class>
+        <filter-name>SeoContentUrlFilter</filter-name>
+        <display-name>SeoContentUrlFilter</display-name>
+        <filter-class>org.ofbiz.product.category.SeoContentUrlFilter</filter-class>
         <init-param>
             <param-name>defaultLocaleString</param-name>
             <param-value>en_US</param-value>
@@ -70,9 +76,9 @@
         <init-param><param-name>viewRequest</param-name><param-value>ViewBlogArticle</param-value></init-param>
     </filter>
     <filter>
-        <filter-name>ContextFilter</filter-name>
-        <display-name>ContextFilter</display-name>
-        <filter-class>org.ofbiz.webapp.control.ContextFilter</filter-class>
+        <filter-name>SeoContextFilter</filter-name>
+        <display-name>SeoContextFilter</display-name>
+        <filter-class>org.ofbiz.product.category.SeoContextFilter</filter-class>
         <init-param>
             <param-name>disableContextSecurity</param-name>
             <param-value>N</param-value>
@@ -83,15 +89,15 @@
         </init-param>
     </filter>
     <filter-mapping>
-        <filter-name>CatalogUrlFilter</filter-name>
+        <filter-name>SeoCatalogUrlFilter</filter-name>
         <url-pattern>/*</url-pattern>
     </filter-mapping>
     <filter-mapping>
-        <filter-name>ContentUrlFilter</filter-name>
+        <filter-name>SeoContentUrlFilter</filter-name>
         <url-pattern>/*</url-pattern>
     </filter-mapping>
     <filter-mapping>
-        <filter-name>ContextFilter</filter-name>
+        <filter-name>SeoContextFilter</filter-name>
         <url-pattern>/*</url-pattern>
     </filter-mapping>
 
@@ -105,10 +111,10 @@
     <listener><listener-class>org.ofbiz.webapp.control.LoginEventListener</listener-class></listener>
 
     <servlet>
-        <servlet-name>ControlServlet</servlet-name>
-        <display-name>ControlServlet</display-name>
-        <description>Main Control Servlet</description>
-        <servlet-class>org.ofbiz.webapp.control.ControlServlet</servlet-class>
+        <servlet-name>SeoControlServlet</servlet-name>
+        <display-name>SeoControlServlet</display-name>
+        <description>Main SEO Control Servlet</description>
+        <servlet-class>org.ofbiz.product.category.SeoControlServlet</servlet-class>
         <load-on-startup>1</load-on-startup>
     </servlet>
     <!-- un-comment for Worldpay
@@ -121,16 +127,16 @@
     </servlet>
     -->
     <servlet>
-        <servlet-name>CatalogUrlServlet</servlet-name>
-        <display-name>CatalogUrlServlet</display-name>
-        <description>Catalog (Category/Product) URL Servlet</description>
-        <servlet-class>org.ofbiz.product.category.CatalogUrlServlet</servlet-class>
+        <servlet-name>SeoCatalogUrlServlet</servlet-name>
+        <display-name>SeoCatalogUrlServlet</display-name>
+        <description>SEO Catalog (Category/Product) URL Servlet</description>
+        <servlet-class>org.ofbiz.product.category.SeoCatalogUrlServlet</servlet-class>
         <load-on-startup>1</load-on-startup>
     </servlet>
 
     <servlet-mapping>
-        <servlet-name>ControlServlet</servlet-name>
-        <url-pattern>/control/*</url-pattern>
+        <servlet-name>SeoControlServlet</servlet-name>
+        <url-pattern>/*</url-pattern>
     </servlet-mapping>
     <!-- un-comment for Worldpay
     <servlet-mapping>
@@ -139,7 +145,7 @@
     </servlet-mapping>
     -->
     <servlet-mapping>
-        <servlet-name>CatalogUrlServlet</servlet-name>
+        <servlet-name>SeoCatalogUrlServlet</servlet-name>
         <url-pattern>/products/*</url-pattern>
     </servlet-mapping>
 
diff --git a/specialpurpose/ecommerce/webapp/ecommerce/index.jsp b/specialpurpose/ecommerce/webapp/ecommerce/index.jsp
index b8ade91..7cb5e40 100644
--- a/specialpurpose/ecommerce/webapp/ecommerce/index.jsp
+++ b/specialpurpose/ecommerce/webapp/ecommerce/index.jsp
@@ -17,4 +17,4 @@
 under the License.
 --%>
 
-<%pageContext.forward("control/main");%>
+<%response.sendRedirect("main");%>