/
Url.java
287 lines (252 loc) · 11.3 KB
/
Url.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
/*
* 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);
}
}