View Javadoc

1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.myfaces.custom.navmenu.jscookmenu;
20  
21  import org.apache.commons.logging.Log;
22  import org.apache.commons.logging.LogFactory;
23  import org.apache.myfaces.custom.navmenu.NavigationMenuItem;
24  import org.apache.myfaces.custom.navmenu.NavigationMenuUtils;
25  import org.apache.myfaces.custom.navmenu.UINavigationMenuItem;
26  import org.apache.myfaces.renderkit.html.util.AddResource;
27  import org.apache.myfaces.renderkit.html.util.AddResourceFactory;
28  import org.apache.myfaces.shared_tomahawk.el.SimpleActionMethodBinding;
29  import org.apache.myfaces.shared_tomahawk.renderkit.JSFAttr;
30  import org.apache.myfaces.shared_tomahawk.renderkit.RendererUtils;
31  import org.apache.myfaces.shared_tomahawk.renderkit.html.HTML;
32  import org.apache.myfaces.shared_tomahawk.renderkit.html.HtmlFormRendererBase;
33  import org.apache.myfaces.shared_tomahawk.renderkit.html.HtmlRenderer;
34  import org.apache.myfaces.shared_tomahawk.renderkit.html.util.FormInfo;
35  import org.apache.myfaces.shared_tomahawk.renderkit.html.util.JavascriptUtils;
36  
37  import javax.faces.FacesException;
38  import javax.faces.component.UIComponent;
39  import javax.faces.context.ExternalContext;
40  import javax.faces.context.FacesContext;
41  import javax.faces.context.ResponseWriter;
42  import javax.faces.el.MethodBinding;
43  import javax.faces.el.ValueBinding;
44  import javax.faces.event.ActionEvent;
45  import java.io.IOException;
46  import java.util.ArrayList;
47  import java.util.List;
48  import java.util.Map;
49  import java.util.StringTokenizer;
50  
51  /**
52   * @JSFRenderer
53   *   renderKitId = "HTML_BASIC" 
54   *   family = "javax.faces.Command"
55   *   type = "org.apache.myfaces.JSCookMenu"
56   * 
57   * @author Thomas Spiegl
58   * @version $Revision: 698742 $ $Date: 2008-09-24 16:25:46 -0500 (Wed, 24 Sep 2008) $
59   */
60  public class HtmlJSCookMenuRenderer
61      extends HtmlRenderer {
62      private static final String MYFACES_HACK_SCRIPT = "MyFacesHack.js";
63  
64      private static final String JSCOOK_MENU_SCRIPT = "JSCookMenu.js";
65      
66      private static final String JSCOOK_EFFECT_SCRIPT = "effect.js";    
67  
68      private static final Log log = LogFactory.getLog(HtmlJSCookMenuRenderer.class);
69  
70      private static final String JSCOOK_ACTION_PARAM = "jscook_action";
71      private static final Class[] ACTION_LISTENER_ARGS = {ActionEvent.class};
72  
73      private static final Map builtInThemes = new java.util.HashMap();
74  
75      static {
76          builtInThemes.put("ThemeOffice", "ThemeOffice/");
77          builtInThemes.put("ThemeMiniBlack", "ThemeMiniBlack/");
78          builtInThemes.put("ThemeIE", "ThemeIE/");
79          builtInThemes.put("ThemePanel", "ThemePanel/");
80          builtInThemes.put("ThemeGray", "ThemeGray/");        
81      }
82  
83      public void decode(FacesContext context, UIComponent component) {
84          RendererUtils.checkParamValidity(context, component, HtmlCommandJSCookMenu.class);
85  
86          Map parameter = context.getExternalContext().getRequestParameterMap();
87          String actionParam = (String) parameter.get(JSCOOK_ACTION_PARAM);
88          if (actionParam != null && !actionParam.trim().equals("") &&
89              !actionParam.trim().equals("null")) {
90              String compId = getMenuId(context, component);
91              StringTokenizer tokenizer = new StringTokenizer(actionParam, ":");
92              if (tokenizer.countTokens() > 1) {
93                  String actionId = tokenizer.nextToken();
94                  if (! compId.equals(actionId)) {
95                      return;
96                  }
97                  while (tokenizer.hasMoreTokens()) {
98                      String action = tokenizer.nextToken();
99                      if (action.startsWith("A]")) {
100                         action = action.substring(2, action.length());
101                         action = decodeValueBinding(action, context);
102                         MethodBinding mb;
103                         if (NavigationMenuUtils.isValueReference(action)) {
104                             mb = context.getApplication().createMethodBinding(action, null);
105                         }
106                         else {
107                             mb = new SimpleActionMethodBinding(action);
108                         }
109                         ((HtmlCommandJSCookMenu) component).setAction(mb);
110                     }
111                     else if (action.startsWith("L]")) {
112                         action = action.substring(2, action.length());
113                         String value = null;
114                         int idx = action.indexOf(";");
115                         if (idx > 0 && idx < action.length() - 1) {
116                             value = action.substring(idx + 1, action.length());
117                             action = action.substring(0, idx);
118                             ((HtmlCommandJSCookMenu) component).setValue(value);
119                         }
120                         else if (idx == action.length() - 1)
121                         {
122                             value = null; //No Value found, so set it to null as expected
123                             action = action.substring(0, idx);
124                         }
125                         MethodBinding mb;
126                         if (NavigationMenuUtils.isValueReference(action)) {
127                             mb = context.getApplication().createMethodBinding(action, ACTION_LISTENER_ARGS);
128                             ((HtmlCommandJSCookMenu) component).setActionListener(mb);
129                             if (value != null)
130                                 ((HtmlCommandJSCookMenu) component).setValue(value);
131                         }
132                     }
133                 }
134             }
135             component.queueEvent(new ActionEvent(component));
136         }
137     }
138 
139     private String decodeValueBinding(String actionParam, FacesContext context) {
140         int idx = actionParam.indexOf(";#{");
141         if (idx == -1) {
142             return actionParam;
143         }
144 
145         String newActionParam = actionParam.substring(0, idx);
146         String vbParam = actionParam.substring(idx + 1);
147 
148         idx = vbParam.indexOf('=');
149         if (idx == -1) {
150             return newActionParam;
151         }
152         String vbExpressionString = vbParam.substring(0, idx);
153         String vbValue = vbParam.substring(idx + 1);
154 
155         ValueBinding vb =
156             context.getApplication().createValueBinding(vbExpressionString);
157         vb.setValue(context, vbValue);
158 
159         return newActionParam;
160     }
161 
162     public boolean getRendersChildren() {
163         return true;
164     }
165 
166     public void encodeChildren(FacesContext context, UIComponent component) throws IOException {
167         RendererUtils.checkParamValidity(context, component, HtmlCommandJSCookMenu.class);
168 
169         List list = NavigationMenuUtils.getNavigationMenuItemList(component);
170         if (list.size() > 0) {
171             FormInfo parentFormInfo = RendererUtils.findNestingForm(component, context);
172             ResponseWriter writer = context.getResponseWriter();
173 
174             if (parentFormInfo == null)
175                 throw new FacesException("jscook menu is not embedded in a form.");
176             String formName = parentFormInfo.getFormName();
177             List uiNavMenuItemList = component.getChildren();
178             /* todo: disabled for now. Check if dummy form stuff is still needed/desired
179                 if( formName == null ) {
180                 DummyFormUtils.setWriteDummyForm(context,true);
181                 DummyFormUtils.addDummyFormParameter(context,JSCOOK_ACTION_PARAM);
182 
183                 formName = DummyFormUtils.getDummyFormName();
184             }
185             else {*/
186             if (RendererUtils.isAdfOrTrinidadForm(parentFormInfo.getForm())) {
187                 // need to add hidden input, cause MyFaces form is missing hence will not render hidden inputs
188                 writer.write("<input type=\"hidden\" name=\"");
189                 writer.write(JSCOOK_ACTION_PARAM);
190                 writer.write("\" />");
191             }
192             else {
193                 HtmlFormRendererBase.addHiddenCommandParameter(context, parentFormInfo.getForm(), JSCOOK_ACTION_PARAM);
194             }
195 
196             //}
197 
198             String myId = getMenuId(context, component);
199 
200             writer.startElement(HTML.SCRIPT_ELEM, component);
201             writer.writeAttribute(HTML.SCRIPT_TYPE_ATTR, HTML.SCRIPT_TYPE_TEXT_JAVASCRIPT, null);
202             StringBuffer script = new StringBuffer();
203             script.append("var ").append(getMenuId(context, component)).append(" =\n[");
204             encodeNavigationMenuItems(context, script,
205                                       (NavigationMenuItem[]) list.toArray(new NavigationMenuItem[list.size()]),
206                                       uiNavMenuItemList,
207                                       myId, formName);
208 
209             script.append("];");
210             writer.writeText(script.toString(), null);
211             writer.endElement(HTML.SCRIPT_ELEM);
212         }
213     }
214 
215     private void encodeNavigationMenuItems(FacesContext context,
216                                            StringBuffer writer,
217                                            NavigationMenuItem[] items,
218                                            List uiNavMenuItemList,
219                                            String menuId, String formName)
220         throws IOException {
221         for (int i = 0; i < items.length; i++) {
222             NavigationMenuItem item = items[i];
223             Object tempObj = null;
224             UINavigationMenuItem uiNavMenuItem = null;
225             if (i < uiNavMenuItemList.size()) {
226                 tempObj = uiNavMenuItemList.get(i);
227             }
228             if (tempObj != null) {
229                 if (tempObj instanceof UINavigationMenuItem) {
230                     uiNavMenuItem = (UINavigationMenuItem) tempObj;
231                 }
232             }
233 
234             if (! item.isRendered()) {
235                 continue;
236             }
237 
238             if (i > 0) {
239                 writer.append(",\n");
240             }
241 
242             if (item.isSplit()) {
243                 writer.append("_cmSplit,");
244 
245                 if (item.getLabel().equals("0")) {
246                     continue;
247                 }
248             }
249 
250             writer.append("[");
251             if (item.getIcon() != null) {
252                 String iconSrc = context.getApplication().getViewHandler().getResourceURL(context, item.getIcon());
253                 writer.append("'<img src=\"");
254                 writer.append(context.getExternalContext().encodeResourceURL(iconSrc));
255                 writer.append("\"/>'");
256             }
257             else {
258                 writer.append("null");
259             }
260             writer.append(", '");
261             if (item.getLabel() != null) {
262                 writer.append(getString(context, item.getLabel()));
263             }
264             writer.append("', ");
265             StringBuffer actionStr = new StringBuffer();
266             if ((item.getAction() != null || item.getActionListener() != null) && ! item.isDisabled()) {
267                 actionStr.append("'");
268                 actionStr.append(menuId);
269                 if (item.getActionListener() != null) {
270                     actionStr.append(":L]");
271                     actionStr.append(item.getActionListener());
272                     if (uiNavMenuItem != null && uiNavMenuItem.getItemValue() != null) {
273                         actionStr.append(';');
274                         actionStr.append(getString(context, uiNavMenuItem.getItemValue()));
275                     }
276                     else if (item.getValue() != null) {
277                         actionStr.append(';');
278                         actionStr.append(getString(context, item.getValue()));
279                     }
280                 }
281                 if (item.getAction() != null) {
282                     actionStr.append(":A]");
283                     actionStr.append(item.getAction());
284                     if (uiNavMenuItem != null) {
285                         encodeValueBinding(actionStr, uiNavMenuItem, item);
286                     }
287                 }
288                 actionStr.append("'");
289                 writer.append(actionStr.toString());
290             }
291             else {
292                 writer.append("null");
293             }
294             writer.append(", '");
295             // Change here to allow the use of non dummy form.
296             writer.append(formName);
297             writer.append("', null");
298 
299             if (item.isRendered() && ! item.isDisabled()) {
300                 // render children only if parent is visible/enabled
301                 NavigationMenuItem[] menuItems = item.getNavigationMenuItems();
302                 if (menuItems != null && menuItems.length > 0) {
303                     writer.append(",");
304                     if (uiNavMenuItem != null) {
305                         encodeNavigationMenuItems(context, writer, menuItems,
306                                                   uiNavMenuItem.getChildren(), menuId, formName);
307                     }
308                     else {
309                         encodeNavigationMenuItems(context, writer, menuItems,
310                                                   new ArrayList(1), menuId, formName);
311                     }
312                 }
313             }
314             writer.append("]");
315         }
316     }
317 
318     private String getString(FacesContext facesContext, Object value) {
319         String str = "";
320 
321         if (value != null) {
322             str = value.toString();
323         }
324 
325         if (NavigationMenuUtils.isValueReference(str)) {
326             value = facesContext.getApplication().createValueBinding(str).getValue(facesContext);
327 
328             if (value != null) {
329                 str = value.toString();
330             }
331             else {
332                 str = "";
333             }
334         }
335 
336         return JavascriptUtils.encodeString(str);
337     }
338 
339     private void encodeValueBinding(StringBuffer writer, UINavigationMenuItem uiNavMenuItem,
340                                     NavigationMenuItem item) {
341         ValueBinding vb = uiNavMenuItem.getValueBinding("NavMenuItemValue");
342         if (vb == null) {
343             return;
344         }
345         String vbExpression = vb.getExpressionString();
346         if (vbExpression == null) {
347             return;
348         }
349         Object tempObj = item.getValue();
350         if (tempObj == null) {
351             return;
352         }
353 
354         writer.append(";");
355         writer.append(vbExpression);
356         writer.append("=");
357         writer.append(tempObj.toString());
358     }
359 
360     public void encodeBegin(FacesContext context, UIComponent component) throws IOException {
361         HtmlCommandJSCookMenu menu = (HtmlCommandJSCookMenu) component;
362         String theme = menu.getTheme();
363         if (theme == null) {
364             // should never happen; theme is a required attribute in the jsp tag definition
365             throw new IllegalArgumentException("theme name is mandatory for a jscookmenu.");
366         }
367 
368         addResourcesToHeader(theme, menu, context);
369     }
370 
371     public void encodeEnd(FacesContext context, UIComponent component) throws IOException {
372         RendererUtils.checkParamValidity(context, component, HtmlCommandJSCookMenu.class);
373         HtmlCommandJSCookMenu menu = (HtmlCommandJSCookMenu) component;
374         String theme = menu.getTheme();
375 
376 
377         ResponseWriter writer = context.getResponseWriter();
378 
379         String menuId = getMenuId(context, component);
380 
381         writer.write("<div id=\"");
382         writer.write(menuId);
383         writer.write("\"></div>\n");
384         writer.startElement(HTML.SCRIPT_ELEM, menu);
385         writer.writeAttribute(HTML.SCRIPT_TYPE_ATTR, HTML.SCRIPT_TYPE_TEXT_JAVASCRIPT, null);
386 
387         StringBuffer buf = new StringBuffer();
388         buf.append("\tif(window.cmDraw!=undefined) { cmDraw ('").
389             append(menuId).
390             append("', ").
391             append(menuId).
392             append(", '").
393             append(menu.getLayout()).
394             append("', cm").
395             append(theme).
396             append(", '").
397             append(theme).
398             append("');}");
399 
400         writer.writeText(buf.toString(), null);
401         writer.endElement(HTML.SCRIPT_ELEM);
402     }
403 
404     private void addResourcesToHeader(String themeName, HtmlCommandJSCookMenu menu, FacesContext context) {
405         String javascriptLocation = (String) menu.getAttributes().get(JSFAttr.JAVASCRIPT_LOCATION);
406         String imageLocation = (String) menu.getAttributes().get(JSFAttr.IMAGE_LOCATION);
407         String styleLocation = (String) menu.getAttributes().get(JSFAttr.STYLE_LOCATION);
408 
409         AddResource addResource = AddResourceFactory.getInstance(context);
410 
411         if (javascriptLocation != null) {
412             addResource.addJavaScriptAtPosition(context, AddResource.HEADER_BEGIN, javascriptLocation + "/" + JSCOOK_MENU_SCRIPT);
413             addResource.addJavaScriptAtPosition(context, AddResource.HEADER_BEGIN, javascriptLocation + "/" + JSCOOK_EFFECT_SCRIPT);            
414             addResource.addJavaScriptAtPosition(context, AddResource.HEADER_BEGIN, javascriptLocation + "/" + MYFACES_HACK_SCRIPT);
415         }
416         else {
417             addResource.addJavaScriptAtPosition(context, AddResource.HEADER_BEGIN, HtmlJSCookMenuRenderer.class, JSCOOK_MENU_SCRIPT);
418             addResource.addJavaScriptAtPosition(context, AddResource.HEADER_BEGIN, HtmlJSCookMenuRenderer.class, JSCOOK_EFFECT_SCRIPT);            
419             addResource.addJavaScriptAtPosition(context, AddResource.HEADER_BEGIN, HtmlJSCookMenuRenderer.class, MYFACES_HACK_SCRIPT);
420         }
421 
422         addThemeSpecificResources(themeName, styleLocation, javascriptLocation, imageLocation, context);
423     }
424 
425     /**
426      * A theme for a menu requires a number of external files; this method
427      * outputs those into the page head section.
428      *
429      * @param themeName          is the name of the theme for this menu. It is never
430      *                           null. It may match one of the built-in theme names or may be a custom
431      *                           theme defined by the application.
432      * @param styleLocation      is the URL of a directory containing a
433      *                           "theme.css" file. A stylesheet link tag will be inserted into
434      *                           the page header referencing that file. If null then if the
435      *                           themeName is a built-in one then a reference to the appropriate
436      *                           built-in stylesheet is generated (requires the ExtensionsFilter).
437      *                           If null and a custom theme is used then no stylesheet link will be
438      *                           generated here.
439      * @param javascriptLocation is the URL of a directory containing a
440      *                           "theme.js" file. A script tag will be inserted into the page header
441      *                           referencing that file. If null then if the themeName is a built-in
442      *                           one then a reference to the built-in stylesheet is generated (requires
443      *                           the ExtensionsFilter). If null and a custom theme is used then no
444      *                           stylesheet link will be generated here.
445      * @param imageLocation      is the URL of a directory containing files
446      *                           (esp. image files) used by the theme.js file to define the menu
447      *                           theme. A javascript variable of name "my{themeName}Base" is
448      *                           generated in the page header containing this URL, so that the
449      *                           theme.js script can locate the files. If null then if the themeName
450      *                           is a built-in one then the URL to the appropriate resource directory
451      *                           is generated (requires the ExtensionsFilter). If null and a custom
452      *                           theme is used then no javascript variable will be generated here.
453      * @param context            is the current faces context.
454      */
455     private void addThemeSpecificResources(String themeName, String styleLocation,
456                                            String javascriptLocation, String imageLocation, FacesContext context) {
457         String themeLocation = (String) builtInThemes.get(themeName);
458         if (themeLocation == null) {
459             log.debug("Unknown theme name '" + themeName + "' specified.");
460         }
461 
462         AddResource addResource = AddResourceFactory.getInstance(context);
463 
464         if ((imageLocation != null) || (themeLocation != null)) {
465             // Generate a javascript variable containing a reference to the
466             // directory containing theme image files, for use by the theme
467             // javascript file. If neither of these is defined (ie a custom
468             // theme was specified but no imageLocation) then presumably the
469             // theme.js file uses some other mechanism to determine where
470             // its image files are.
471             StringBuffer buf = new StringBuffer();
472             buf.append("var my");
473             buf.append(themeName);
474             buf.append("Base='");
475             ExternalContext externalContext = context.getExternalContext();
476             if (imageLocation != null) {
477                 buf.append(externalContext.encodeResourceURL(addResource.getResourceUri(context,
478                                                                                         imageLocation + "/" + themeName)));
479             }
480             else {
481                 buf.append(externalContext.encodeResourceURL(addResource.getResourceUri(context,
482                                                                                         HtmlJSCookMenuRenderer.class, themeLocation)));
483             }
484             buf.append("';");
485             addResource.addInlineScriptAtPosition(context, AddResource.HEADER_BEGIN, buf.toString());
486         }
487 
488 
489         if ((javascriptLocation != null) || (themeLocation != null)) {
490             // Generate a <script> tag in the page header pointing to the
491             // theme.js file for this theme. If neither of these is defined
492             // then presumably the theme.js file is referenced by a <script>
493             // tag hard-wired into the page or inserted via some other means.
494             if (javascriptLocation != null) {
495                 // For now, assume that if the user specified a location for a custom
496                 // version of the jscookMenu.js file then the theme.js file can be found
497                 // in the same location.
498                 addResource.addJavaScriptAtPosition(context, AddResource.HEADER_BEGIN, javascriptLocation + "/" + themeName
499                     + "/theme.js");
500             }
501             else {
502                 // Using a built-in theme, so we know where the theme.js file is.
503                 addResource.addJavaScriptAtPosition(context, AddResource.HEADER_BEGIN, HtmlJSCookMenuRenderer.class, themeName
504                     + "/theme.js");
505             }
506         }
507 
508         if ((styleLocation != null) || (themeLocation != null)) {
509             // Generate a <link type="text/css"> tag in the page header pointing to
510             // the theme stylesheet. If neither of these is defined then presumably
511             // the stylesheet is referenced by a <link> tag hard-wired into the page
512             // or inserted via some other means.
513             if (styleLocation != null) {
514                 addResource.addStyleSheet(context, AddResource.HEADER_BEGIN, styleLocation + "/" + themeName + "/theme.css");
515             }
516             else {
517                 addResource.addStyleSheet(context, AddResource.HEADER_BEGIN, HtmlJSCookMenuRenderer.class, themeName
518                     + "/theme.css");
519             }
520         }
521     }
522 
523     /**
524      * Fetch the very last part of the menu id.
525      *
526      * @param context
527      * @param component
528      * @return String id of the menu
529      */
530     private String getMenuId(FacesContext context, UIComponent component) {
531         String menuId = component.getClientId(context).replaceAll(":", "_") + "_menu";
532         while (menuId.startsWith("_")) {
533             menuId = menuId.substring(1);
534         }
535         return menuId;
536     }
537 
538 }