View Javadoc
1   /*
2    * $Id$
3    *
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *  http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  package org.apache.struts.faces.renderer;
23  
24  import java.io.IOException;
25  import java.util.ArrayList;
26  import java.util.Collections;
27  import java.util.Comparator;
28  import java.util.Iterator;
29  import java.util.List;
30  import java.util.Locale;
31  import java.util.Map;
32  import java.util.function.Function;
33  
34  import org.apache.commons.validator.Field;
35  import org.apache.commons.validator.Form;
36  import org.apache.commons.validator.ValidatorAction;
37  import org.apache.commons.validator.ValidatorResources;
38  import org.apache.commons.validator.Var;
39  import org.apache.commons.validator.util.ValidatorUtils;
40  import org.apache.struts.Globals;
41  import org.apache.struts.config.ModuleConfig;
42  import org.apache.struts.faces.component.FormComponent;
43  import org.apache.struts.faces.component.JavascriptValidatorComponent;
44  import org.apache.struts.faces.util.StrutsContext;
45  import org.apache.struts.faces.util.Utils;
46  import org.apache.struts.util.MessageResources;
47  import org.apache.struts.validator.Resources;
48  import org.apache.struts.validator.ValidatorPlugIn;
49  
50  import jakarta.faces.component.UIComponent;
51  import jakarta.faces.context.FacesContext;
52  import jakarta.faces.context.ResponseWriter;
53  
54  /**
55   * {@code Renderer} implementation for the
56   * {@code JavascriptValidator} tag from the
57   * <em>Struts-Faces Integration Library</em>.</p>
58   *
59   * @version $Rev$ $Date$
60   */
61  public class JavascriptValidatorRenderer extends AbstractRenderer {
62  
63      private final static Function<ValidatorAction, Integer> SIZE_VA = (va) ->
64          va == null || va.getDepends() == null ? 0 : va.getDepends().length();
65  
66      private final static Comparator<ValidatorAction> COMP_VA = (va1, va2) -> {
67          final int size1 = SIZE_VA.apply(va1);
68          final int size2 = SIZE_VA.apply(va2);
69  
70          if (size1 == 0) {
71              return size2 == 0 ? 0 : -1;
72          } else if (size2 == 0) {
73              return 1;
74          }
75          return va1.getDependencyList().size()
76                  - va2.getDependencyList().size();
77      };
78  
79      private final static String HTML_BEGIN_COMMENT = "\n<!-- Begin \n";
80  
81      private final static String HTML_END_COMMENT = "//End --> \n";
82  
83  
84      // ---------------------------------------------------------- Public Methods
85  
86  
87      /**
88       * Render the beginning {@code script} tag.
89       *
90       * @param context FacesContext for the current request
91       * @param component UIComponent to be rendered
92       *
93       * @exception IOException if an input/output error occurs while rendering
94       * @exception NullPointerException if {@code context}
95       *  or {@code component} is {@code null}
96       */
97      protected void renderEnd(FacesContext context, UIComponent component,
98              ResponseWriter writer) throws IOException {
99  
100         if (context == null || component == null) {
101             throw new NullPointerException();
102         }
103 
104         final StringBuilder results = new StringBuilder();
105 
106         final StrutsContext strutsContext = new StrutsContext(context);
107         JavascriptValidatorComponent jsv = (JavascriptValidatorComponent) component;
108 
109         ModuleConfig config = strutsContext.getModuleConfig();
110 
111         ValidatorResources resources = Utils.getMapValue(ValidatorResources.class,
112                 context.getExternalContext().getApplicationMap(),
113                 ValidatorPlugIn.VALIDATOR_KEY + config.getPrefix());
114 
115         Locale locale = strutsContext.getLocale();
116 
117         // Look up the MessageResources bundle to be used
118         String bundle = jsv.getBundle();
119         if (bundle == null) {
120             bundle = Globals.MESSAGES_KEY;
121         }
122 
123         Form form = resources.getForm(locale, jsv.getFormName());
124         if (form != null) {
125             if (jsv.isDynamicJavascript()) {
126                 MessageResources messages = Utils.getMapValue(MessageResources.class,
127                         context.getExternalContext().getApplicationMap(), bundle);
128 
129                 List<ValidatorAction> lActions = new ArrayList<>();
130                 List<String> lActionMethods = new ArrayList<>();
131 
132                 // Get List of actions for this Form
133                 for (Field field : form.getFields()) {
134 
135                     for (String depends : field.getDependencyList()) {
136 
137                         if (depends != null && !lActionMethods.contains(depends)) {
138                             lActionMethods.add(depends);
139                         }
140                     }
141 
142                 }
143 
144                 // Create list of ValidatorActions based on lActionMethods
145                 for (Iterator<String> i = lActionMethods.iterator(); i.hasNext();) {
146                     String depends = i.next();
147                     ValidatorAction va = resources.getValidatorAction(depends);
148 
149                     // throw nicer NPE for easier debugging
150                     if (va == null) {
151                         throw new NullPointerException(
152                             "Depends string \""
153                                 + depends
154                                 + "\" was not found in validator-rules.xml.");
155                     }
156 
157                     String javascript = va.getJavascript();
158                     if (javascript != null && javascript.length() > 0) {
159                         lActions.add(va);
160                     } else {
161                         i.remove();
162                     }
163                 }
164 
165                 Collections.sort(lActions, COMP_VA);
166 
167                 String methods = null;
168                 for (ValidatorAction va : lActions) {
169 
170                     if (methods == null) {
171                         methods = va.getMethod() + "(form)";
172                     } else {
173                         methods += " && " + va.getMethod() + "(form)";
174                     }
175                 }
176 
177                 results.append(getJavascriptBegin(methods, jsv));
178 
179                 final String formClientId = getFormClientId(jsv);
180                 final String formClientIdFunc = formClientId.replace(':', '_');
181 
182                 for (ValidatorAction va : lActions) {
183                     String jscriptVar = null;
184                     String functionName = null;
185 
186                     if (va.getJsFunctionName() != null
187                         && va.getJsFunctionName().length() > 0) {
188                         functionName = va.getJsFunctionName();
189                     } else {
190                         functionName = va.getName();
191                     }
192 
193                     results.append("    function " +
194                       formClientIdFunc + "_" + functionName +
195                       " () { \n");
196 
197                     for (Field field : form.getFields()) {
198 
199                         // Skip indexed fields for now until there is a good
200                         // way to handle error messages (and the length of the
201                         // list (could retrieve from scope?))
202                         if (field.isIndexed()
203                             || field.getPage() != jsv.getPage()
204                             || !field.isDependency(va.getName())) {
205 
206                             continue;
207                         }
208 
209                         String message =
210                             Resources.getMessage(messages, locale, va, field);
211 
212                         message = (message != null) ? message : "";
213 
214                         jscriptVar = this.getNextVar(jscriptVar);
215 
216                         results.append(
217                             "     this."
218                                 + jscriptVar
219                                 + " = new Array(\""
220                                 + formClientId
221                                 + ":"
222                                 + field.getKey()
223                                 + "\", \""
224                                 + message
225                                 + "\", ");
226 
227                         results.append("new Function (\"varName\", \"");
228 
229                         Map<String, Var> vars = field.getVars();
230                         // Loop through the field's variables.
231                         for (Map.Entry<String, Var> entry : vars.entrySet()) {
232                             String varName = entry.getKey();
233                             Var var = entry.getValue();
234                             String varValue = var.getValue();
235                             String jsType = var.getJsType();
236 
237                             // skip requiredif variables field, fieldIndexed,
238                             // fieldTest, fieldValue
239                             if (varName.startsWith("field")) {
240                                 continue;
241                             }
242 
243                             if (Var.JSTYPE_INT.equalsIgnoreCase(jsType)) {
244                                 results.append(
245                                     "this."
246                                         + varName
247                                         + "="
248                                         + ValidatorUtils.replace(
249                                             varValue,
250                                             "\\",
251                                             "\\\\")
252                                         + "; ");
253                             } else if (Var.JSTYPE_REGEXP.equalsIgnoreCase(
254                                 jsType)) {
255                                 results.append(
256                                     "this."
257                                         + varName
258                                         + "=/"
259                                         + ValidatorUtils.replace(
260                                             varValue,
261                                             "\\",
262                                             "\\\\")
263                                         + "/; ");
264                             } else if (Var.JSTYPE_STRING.equalsIgnoreCase(
265                                 jsType)) {
266                                 results.append(
267                                     "this."
268                                         + varName
269                                         + "='"
270                                         + ValidatorUtils.replace(
271                                             varValue,
272                                             "\\",
273                                             "\\\\")
274                                         + "'; ");
275                                 // So everyone using the latest format doesn't
276                                 // need to change their xml files immediately.
277                             } else if ("mask".equalsIgnoreCase(varName)) {
278                                 results.append(
279                                     "this."
280                                         + varName
281                                         + "=/"
282                                         + ValidatorUtils.replace(
283                                             varValue,
284                                             "\\",
285                                             "\\\\")
286                                         + "/; ");
287                             } else {
288                                 results.append(
289                                     "this."
290                                         + varName
291                                         + "='"
292                                         + ValidatorUtils.replace(
293                                             varValue,
294                                             "\\",
295                                             "\\\\")
296                                         + "'; ");
297                             }
298                         }
299 
300                         results.append(" return this[varName];\"));\n");
301                     }
302                     results.append("    } \n\n");
303                 }
304             } else if (jsv.isStaticJavascript()) {
305                 results.append(getStartElement(jsv));
306                 if (jsv.isHtmlComment()) {
307                     results.append(HTML_BEGIN_COMMENT);
308                 }
309             }
310         }
311 
312         if (jsv.isStaticJavascript()) {
313             results.append(getJavascriptStaticMethods(resources));
314         }
315 
316         if (form != null
317                 && (jsv.isDynamicJavascript() || jsv.isStaticJavascript())) {
318 
319             results.append(getJavascriptEnd(jsv));
320         }
321 
322         writer.write(results.toString());
323     }
324 
325     // ----------------------------------------------------------- Properties
326 
327     /**
328      * Returns the opening script element and some initial JavaScript.
329      */
330     protected String getJavascriptBegin(String methods, JavascriptValidatorComponent component) {
331         StringBuilder sb = new StringBuilder();
332         String name =
333             component.getFormName().substring(0, 1).toUpperCase()
334                 + component.getFormName().substring(1);
335 
336         sb.append(getStartElement(component));
337 
338         if (isXhtml(component) && component.isCdata()) {
339             sb.append("<![CDATA[\r\n");
340         }
341 
342         if (!isXhtml(component) && component.isHtmlComment()) {
343             sb.append(HTML_BEGIN_COMMENT);
344         }
345         sb.append("\n     var bCancel = false; \n\n");
346 
347         if (component.getMethod() == null || component.getMethod().isEmpty()) {
348             sb.append(
349                 "    function validate"
350                     + name
351                     + "(form) {                                          "
352                     + "                         \n");
353         } else {
354             sb.append(
355                 "    function "
356                     + component.getMethod()
357                     + "(form) {                                          "
358                     + "                         \n");
359         }
360         sb.append("        if (bCancel) \n");
361         sb.append("      return true; \n");
362         sb.append("        else \n");
363 
364         // Always return true if there aren't any JavaScript validation methods
365         if (methods == null || methods.isEmpty()) {
366             sb.append("       return true; \n");
367         } else {
368             sb.append("       return " + methods + "; \n");
369         }
370 
371         sb.append("   } \n\n");
372 
373         return sb.toString();
374     }
375 
376     protected String getJavascriptStaticMethods(ValidatorResources resources) {
377         StringBuilder sb = new StringBuilder();
378 
379         sb.append("\n\n");
380 
381         for (ValidatorAction va : resources.getValidatorActions().values()) {
382             if (va != null) {
383                 String javascript = va.getJavascript();
384                 if (javascript != null && javascript.length() > 0) {
385                     sb.append(javascript + "\n");
386                 }
387             }
388         }
389 
390         return sb.toString();
391     }
392 
393     /**
394      * Returns the closing script element.
395      */
396     protected String getJavascriptEnd(JavascriptValidatorComponent component) {
397         StringBuilder sb = new StringBuilder();
398 
399         sb.append("\n");
400         if (!isXhtml(component) && component.isHtmlComment()){
401             sb.append(HTML_END_COMMENT);
402         }
403 
404         if (isXhtml(component) && component.isCdata()) {
405             sb.append("]]>\r\n");
406         }
407 
408         sb.append("</script>\n\n");
409 
410         return sb.toString();
411     }
412 
413     /**
414      * The value {@code null} will be returned at the end of the sequence.
415      * &nbsp;&nbsp;&nbsp; ex: "zz" will return {@code null}
416      */
417     private String getNextVar(String input) {
418         if (input == null) {
419             return "aa";
420         }
421 
422         input = input.toLowerCase();
423 
424         for (int i = input.length(); i > 0; i--) {
425             int pos = i - 1;
426 
427             char c = input.charAt(pos);
428             c++;
429 
430             if (c <= 'z') {
431                 if (i == 0) {
432                     return c + input.substring(pos, input.length());
433                 } else if (i == input.length()) {
434                     return input.substring(0, pos) + c;
435                 } else {
436                     return input.substring(0, pos) + c + input.substring(pos,
437                       input.length() - 1);
438                 }
439             } else {
440                 input = replaceChar(input, pos, 'a');
441             }
442 
443         }
444 
445         return null;
446 
447     }
448 
449     /**
450      * Replaces a single character in a {@code String}
451      */
452     private String replaceChar(String input, int pos, char c) {
453         if (pos == 0) {
454             return c + input.substring(pos, input.length());
455         } else if (pos == input.length()) {
456             return input.substring(0, pos) + c;
457         } else {
458             return input.substring(0, pos) + c + input.substring(pos,
459               input.length() - 1);
460         }
461     }
462 
463     /**
464      * Constructs the beginning &lt;script&gt; element depending on
465      * xhtml status.
466      */
467     private String getStartElement(JavascriptValidatorComponent component) {
468         StringBuilder start =
469           new StringBuilder("<script type=\"text/javascript\"");
470 
471         // there is no language attribute in xhtml
472         if (!isXhtml(component)) {
473             start.append(" language=\"Javascript\"");
474         }
475 
476         if (component.getSrc() != null) {
477             start.append(" src=\"" + component.getSrc() + "\"");
478         }
479 
480         start.append("> \n");
481         return start.toString();
482     }
483 
484 
485     /**
486      * <p>Return the {@code clientId} of the form component for which
487      * we are rendering validation Javascript.</p>
488      *
489      * @exception IllegalStateException if we are not nested inside a
490      *  UIComponentTag with a child FormComponent matching our form name
491      */
492     private String getFormClientId(final JavascriptValidatorComponent component){
493         final String formName = component.getFormName();
494 
495         for (UIComponent current = component; current != null; current = current.getParent()) {
496             List<UIComponent> kids = current.getChildren();
497             for (UIComponent kid : kids) {
498                 if (kid instanceof FormComponent
499                         && formName.equals(kid.getAttributes().get("beanName"))) {
500 
501                     return kid.getClientId(FacesContext.getCurrentInstance());
502                 }
503             }
504         }
505 
506         throw new IllegalArgumentException
507             ("Cannot find child FormComponent for form '" + formName + "'");
508     }
509 }