/*
* Copyright 2017 OmniFaces
*
* Licensed 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.omnifaces.component.output;
import static java.lang.Boolean.FALSE;
import static java.lang.String.format;
import static org.omnifaces.util.Components.getParams;
import static org.omnifaces.util.Faces.getRequestDomainURL;
import static org.omnifaces.util.FacesLocal.getBookmarkableURL;
import static org.omnifaces.util.FacesLocal.getRequestDomainURL;
import static org.omnifaces.util.FacesLocal.getRequestURI;
import static org.omnifaces.util.FacesLocal.setRequestAttribute;
import static org.omnifaces.util.ResourcePaths.stripTrailingSlash;
import static org.omnifaces.util.Servlets.toQueryString;
import static org.omnifaces.util.Utils.isEmpty;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.List;
import java.util.Map;
import javax.el.ValueExpression;
import javax.faces.component.FacesComponent;
import javax.faces.context.FacesContext;
import org.omnifaces.util.State;
/**
* <p>
* The <code><o:url></code> is a component which renders the given JSF view ID as a bookmarkable URL with support
* for exposing it into the request scope by the variable name as specified by the <code>var</code> attribute instead of
* rendering it.
* <p>
* This component also supports adding query string parameters to the URL via nested <code><f:param></code> and
* <code><o:param></code>. This can be used in combination with <code>includeViewParams</code> and
* <code>includeRequestParams</code>. The <code><f|o:param></code> will override any included view or request
* parameters on the same name. To conditionally add or override, use the <code>disabled</code> attribute of
* <code><f|o:param></code>.
* <p>
* This component fills the gap caused by absence of JSTL <code><c:url></code> in Facelets. This component is
* useful for generating URLs for usage in e.g. plain HTML <code><link></code> elements and JavaScript variables.
*
* <h3>Domain</h3>
* <p>
* The domain of the URL defaults to the current domain. It is possible to provide a full qualified domain name (FQDN)
* via the <code>domain</code> attribute which the URL is to be prefixed with. This can be useful if a canonical page
* shall point to a different domain or a specific subdomain.
* <p>
* Valid formats and values for <code>domain</code> attribute are:
* <ul>
* <li><code><o:url domain="http://example.com" /></code></li>
* <li><code><o:url domain="//example.com" /></code></li>
* <li><code><o:url domain="example.com" /></code></li>
* <li><code><o:url domain="/" /></code></li>
* <li><code><o:url domain="//" /></code></li>
* </ul>
* <p>
* The <code>domain</code> value will be validated by {@link URL} and throw an illegal argument exception when invalid.
* If the value equals <code>/</code>, then the URL becomes domain-relative.
* If the value equals or starts with <code>//</code>, or does not contain any scheme, then the URL becomes scheme-relative.
*
* <h3>Request and view parameters</h3>
* <p>You can optionally include all GET request query string parameters or only JSF view parameters in the resulting
* URL via <code>includeRequestParams="true"</code> or <code>includeViewParams="true"</code>.
* The <code>includeViewParams</code> is ignored when <code>includeRequestParams="true"</code>.
* The <code><f|o:param></code> will override any included request or view parameters on the same name.
*
*
* <h3>Usage</h3>
* <p>
* Some examples:
* <pre>
* <p>Full URL of current page is: <o:url /></p>
* <p>Full URL of another page is: <o:url viewId="/another.xhtml" /></p>
* <p>Full URL of current page including view params is: <o:url includeViewParams="true" /></p>
* <p>Full URL of current page including query string is: <o:url includeRequestParams="true" /></p>
* <p>Domain-relative URL of current page is: <o:url domain="/" /></p>
* <p>Scheme-relative URL of current page is: <o:url domain="//" /></p>
* <p>Scheme-relative URL of current page on a different domain is: <o:url domain="sub.example.com" /></p>
* <p>Full URL of current page on a different domain is: <o:url domain="https://sub.example.com" /></p>
* </pre>
* <pre>
* <o:url var="_linkCanonical">
* <o:param name="foo" value="#{bean.foo}" />
* </o:url>
* <link rel="canonical" href="#{_linkCanonical}" />
* </pre>
* <pre>
* <o:url var="_linkNext" includeViewParams="true">
* <f:param name="page" value="#{bean.pageIndex + 1}" />
* </o:url>
* <link rel="next" href="#{_linkNext}" />
* </pre>
*
* @author Bauke Scholtz
* @since 2.4
* @see OutputFamily
*/
@FacesComponent(Url.COMPONENT_TYPE)
public class Url extends OutputFamily {
// Public constants -----------------------------------------------------------------------------------------------
/** The standard component type. */
public static final String COMPONENT_TYPE = "org.omnifaces.component.output.Url";
// Private constants ----------------------------------------------------------------------------------------------
private static final String ERROR_EXPRESSION_DISALLOWED =
"A value expression is disallowed on 'var' attribute of Url.";
private static final String ERROR_INVALID_DOMAIN =
"o:url 'domain' attribute '%s' does not represent a valid domain.";
private enum PropertyKeys {
// Cannot be uppercased. They have to exactly match the attribute names.
var,
domain,
viewId,
includeViewParams,
includeRequestParams;
}
// Variables ------------------------------------------------------------------------------------------------------
private final State state = new State(getStateHelper());
// Actions --------------------------------------------------------------------------------------------------------
/**
* An override which checks if this isn't been invoked on <code>var</code> attribute.
* Finally it delegates to the super method.
* @throws IllegalArgumentException When this value expression is been set on <code>var</code> attribute.
*/
@Override
public void setValueExpression(String name, ValueExpression binding) {
if (PropertyKeys.var.toString().equals(name)) {
throw new IllegalArgumentException(ERROR_EXPRESSION_DISALLOWED);
}
super.setValueExpression(name, binding);
}
/**
* Returns <code>false</code>.
*/
@Override
public boolean getRendersChildren() {
return false;
}
@Override
public void encodeEnd(FacesContext context) throws IOException {
String viewId = getViewId();
Map<String, List<String>> params = getParams(this, isIncludeRequestParams(), isIncludeViewParams());
String url = (viewId == null) ? getActionURL(context, params) : getBookmarkableURL(context, viewId, params, false);
String domain = getDomain();
if ("//".equals(domain)) {
url = getRequestDomainURL(context).split(":", 2)[1] + url;
}
else if (!"/".equals(domain)) {
String normalizedDomain = domain.contains("//") ? domain : ("//") + domain;
try {
new URL(normalizedDomain.startsWith("//") ? ("http:" + normalizedDomain) : normalizedDomain);
}
catch (MalformedURLException e) {
throw new IllegalArgumentException(format(ERROR_INVALID_DOMAIN, domain), e);
}
url = stripTrailingSlash(normalizedDomain) + url;
}
if (getVar() != null) {
setRequestAttribute(context, getVar(), url);
}
else {
context.getResponseWriter().writeText(url, null);
}
}
private static String getActionURL(FacesContext context, Map<String, List<String>> params) {
String url = getRequestURI(context);
url = url.isEmpty() ? "/" : url;
String queryString = toQueryString(params);
return isEmpty(queryString) ? url : url + (url.contains("?") ? "&" : "?") + queryString;
}
// Attribute getters/setters --------------------------------------------------------------------------------------
/**
* Returns the variable name which exposes the URL into the request scope.
* @return The variable name which exposes the URL into the request scope.
*/
public String getVar() {
return state.get(PropertyKeys.var);
}
/**
* Sets the variable name which exposes the URL into the request scope.
* @param var The variable name which exposes the URL into the request scope.
*/
public void setVar(String var) {
state.put(PropertyKeys.var, var);
}
/**
* Returns the domain of the URL. Defaults to current domain.
* @return The domain of the URL.
*/
public String getDomain() {
return state.get(PropertyKeys.domain, getRequestDomainURL());
}
/**
* Sets the domain of the URL.
* @param domain The domain of the URL.
*/
public void setDomain(String domain) {
state.put(PropertyKeys.domain, domain);
}
/**
* Returns the view ID to create URL for. Defaults to current view ID.
* @return The view ID to create URL for.
*/
public String getViewId() {
return state.get(PropertyKeys.viewId);
}
/**
* Sets the view ID to create URL for.
* @param viewId The view ID to create URL for.
*/
public void setViewId(String viewId) {
state.put(PropertyKeys.viewId, viewId);
}
/**
* Returns whether or not the view parameters should be encoded into the URL. Defaults to <code>false</code>.
* This setting is ignored when <code>includeRequestParams</code> is set to <code>true</code>.
* @return Whether or not the view parameters should be encoded into the URL.
*/
public boolean isIncludeViewParams() {
return state.get(PropertyKeys.includeViewParams, FALSE);
}
/**
* Sets whether or not the view parameters should be encoded into the URL.
* This setting is ignored when <code>includeRequestParams</code> is set to <code>true</code>.
* @param includeViewParams Whether or not the view parameters should be encoded into the URL.
*/
public void setIncludeViewParams(boolean includeViewParams) {
state.put(PropertyKeys.includeViewParams, includeViewParams);
}
/**
* Returns whether or not the request query string parameters should be encoded into the URL. Defaults to <code>false</code>.
* When set to <code>true</code>, then this will override the <code>includeViewParams</code> setting.
* @return Whether or not the request query string parameters should be encoded into the URL.
*/
public boolean isIncludeRequestParams() {
return state.get(PropertyKeys.includeRequestParams, FALSE);
}
/**
* Sets whether or not the request query string parameters should be encoded into the URL.
* When set to <code>true</code>, then this will override the <code>includeViewParams</code> setting.
* @param includeRequestParams Whether or not the request query string parameters should be encoded into the URL.
*/
public void setIncludeRequestParams(boolean includeRequestParams) {
state.put(PropertyKeys.includeRequestParams, includeRequestParams);
}
}