/
NoAutoGeneratedIdViewHandler.java
172 lines (140 loc) · 5.93 KB
/
NoAutoGeneratedIdViewHandler.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
/*
* 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.viewhandler;
import static java.lang.String.format;
import static javax.faces.component.UINamingContainer.getSeparatorChar;
import static javax.faces.component.UIViewRoot.UNIQUE_ID_PREFIX;
import static org.omnifaces.util.Components.findComponent;
import static org.omnifaces.util.Faces.getContext;
import static org.omnifaces.util.FacesLocal.isDevelopment;
import java.io.IOException;
import java.io.Writer;
import javax.faces.application.Application;
import javax.faces.application.ProjectStage;
import javax.faces.application.ViewHandler;
import javax.faces.application.ViewHandlerWrapper;
import javax.faces.component.UIComponent;
import javax.faces.component.UIViewRoot;
import javax.faces.context.FacesContext;
import javax.faces.context.FacesContextWrapper;
import javax.faces.context.ResponseWriter;
import javax.faces.context.ResponseWriterWrapper;
/**
* <p>
* This {@link ViewHandler} once installed will during development stage throw an {@link IllegalStateException} whenever
* an automatically generated JSF component ID (<code>j_id...</code>) is encountered in the rendered output.
* This has various advantages:
* <ul>
* <li>Keep the HTML output free of autogenerated JSF component IDs.
* <li>No need to fix the IDs again and again when the client side unit tester encounters an unusable autogenerated ID.
* <li>Make the developer aware which components are naming containers and/or implicitly require outputting its ID.
* </ul>
* <p>
* Note that this does not check every component for its ID directly, but instead checks the {@link ResponseWriter} for
* writes to the "id" attribute. Components that write their markup in any other way won't be checked and will thus
* slip through.
*
* <h3>Installation</h3>
* <p>
* Register it as <code><view-handler></code> in <code>faces-config.xml</code>.
* <pre>
* <application>
* <view-handler>org.omnifaces.viewhandler.NoAutoGeneratedIdViewHandler</view-handler>
* </application>
* </pre>
* <p>
* Note that this only runs if {@link Application#getProjectStage()} equals to {@link ProjectStage#Development}.
*
* @since 2.0
* @author Arjan Tijms
*/
public class NoAutoGeneratedIdViewHandler extends ViewHandlerWrapper {
// Private constants ----------------------------------------------------------------------------------------------
private static final String ERROR_AUTO_GENERATED_ID_ENCOUNTERED =
"Auto generated ID '%s' encountered on component type: '%s'.";
// Properties -----------------------------------------------------------------------------------------------------
private ViewHandler wrapped;
// Constructors ---------------------------------------------------------------------------------------------------
/**
* Construct a new No Auto Generated Id view handler around the given wrapped view handler.
*
* @param wrapped
* The wrapped view handler.
*/
public NoAutoGeneratedIdViewHandler(ViewHandler wrapped) {
this.wrapped = wrapped;
}
// Actions --------------------------------------------------------------------------------------------------------
@Override
public void renderView(final FacesContext context, UIViewRoot viewToRender) throws IOException {
super.renderView(isDevelopment(context) ? new FacesContextWrapper() {
@Override
public void setResponseWriter(ResponseWriter responseWriter) {
super.setResponseWriter(new NoAutoGeneratedIdResponseWriter(responseWriter));
}
@Override
public FacesContext getWrapped() {
return context;
}
} : context, viewToRender);
}
@Override
public ViewHandler getWrapped() {
return wrapped;
}
// Nested classes -------------------------------------------------------------------------------------------------
/**
* This response writer throws an {@link IllegalStateException} when an attribute with name "id" is written with
* a non-null value which starts with {@link UIViewRoot#UNIQUE_ID_PREFIX} or contains an intermediate.
*
* @since 2.0
* @author Arjan Tijms
*/
public static class NoAutoGeneratedIdResponseWriter extends ResponseWriterWrapper {
private ResponseWriter wrapped;
private final char separatorChar;
private final String intermediateIdPrefix;
public NoAutoGeneratedIdResponseWriter(ResponseWriter wrapped) {
this.wrapped = wrapped;
separatorChar = getSeparatorChar(getContext());
intermediateIdPrefix = separatorChar + UNIQUE_ID_PREFIX;
}
@Override
public ResponseWriter cloneWithWriter(Writer writer) {
return new NoAutoGeneratedIdResponseWriter(super.cloneWithWriter(writer));
}
@Override
public void writeAttribute(String name, Object value, String property) throws IOException {
if (value != null && "id".equals(name)) {
String id = value.toString();
if (id.startsWith(UNIQUE_ID_PREFIX) || id.contains(intermediateIdPrefix)) {
int end = id.indexOf(separatorChar, id.indexOf(UNIQUE_ID_PREFIX));
if (end > 0) {
id = id.substring(0, end);
}
UIComponent component = findComponent(id);
if (!(component instanceof UIViewRoot)) { // Skip viewstate hidden inputs.
throw new IllegalStateException(format(ERROR_AUTO_GENERATED_ID_ENCOUNTERED,
id, (component == null) ? "<null>" : component.getClass().getName()
));
}
}
}
super.writeAttribute(name, value, property);
}
@Override
public ResponseWriter getWrapped() {
return wrapped;
}
}
}