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.shared.renderkit;
20  
21  import java.io.ByteArrayOutputStream;
22  import java.io.FileNotFoundException;
23  import java.io.IOException;
24  import java.io.InputStream;
25  import java.io.Serializable;
26  import java.lang.reflect.Array;
27  import java.util.ArrayList;
28  import java.util.Collection;
29  import java.util.Collections;
30  import java.util.Date;
31  import java.util.HashSet;
32  import java.util.Iterator;
33  import java.util.List;
34  import java.util.Map;
35  import java.util.Set;
36  import java.util.logging.Level;
37  import java.util.logging.Logger;
38  
39  import javax.el.ValueExpression;
40  import javax.faces.FacesException;
41  import javax.faces.FactoryFinder;
42  import javax.faces.application.FacesMessage;
43  import javax.faces.application.ProjectStage;
44  import javax.faces.application.Resource;
45  import javax.faces.application.ResourceHandler;
46  import javax.faces.component.EditableValueHolder;
47  import javax.faces.component.NamingContainer;
48  import javax.faces.component.UIComponent;
49  import javax.faces.component.UIForm;
50  import javax.faces.component.UIInput;
51  import javax.faces.component.UIOutput;
52  import javax.faces.component.UISelectMany;
53  import javax.faces.component.UISelectOne;
54  import javax.faces.component.UIViewRoot;
55  import javax.faces.component.ValueHolder;
56  import javax.faces.component.html.HtmlInputText;
57  import javax.faces.context.FacesContext;
58  import javax.faces.convert.Converter;
59  import javax.faces.convert.ConverterException;
60  import javax.faces.el.PropertyNotFoundException;
61  import javax.faces.el.ValueBinding;
62  import javax.faces.event.PhaseId;
63  import javax.faces.model.SelectItem;
64  import javax.faces.render.RenderKit;
65  import javax.faces.render.RenderKitFactory;
66  import javax.faces.render.ResponseStateManager;
67  
68  import org.apache.myfaces.shared.renderkit.html.util.FormInfo;
69  import org.apache.myfaces.shared.util.HashMapUtils;
70  import org.apache.myfaces.shared.util.SelectItemsIterator;
71  
72  public final class RendererUtils
73  {
74      private RendererUtils()
75      {
76          //nope
77      }
78  
79      //private static final Log log = LogFactory.getLog(RendererUtils.class);
80      private static final Logger log = Logger.getLogger(RendererUtils.class
81              .getName());
82  
83      public static final String SELECT_ITEM_LIST_ATTR = RendererUtils.class
84              .getName() + ".LIST";
85      public static final String EMPTY_STRING = "";
86      //This constant is no longer used by UISelectOne/UISelectMany instances
87      public static final Object NOTHING = new Serializable()
88      {
89          public boolean equals(final Object o)
90          {
91              if (o != null)
92              {
93                  if (o.getClass().equals(this.getClass()))
94                  {
95                      return true;
96                  }
97              }
98              return false;
99          }
100 
101         @Override
102         public int hashCode()
103         {
104             return super.hashCode();
105         }
106     };
107 
108     public static final String ACTION_FOR_LIST = "org.apache.myfaces.ActionForList";
109     public static final String ACTION_FOR_PHASE_LIST = "org.apache.myfaces.ActionForPhaseList";
110 
111     public static final String SEQUENCE_PARAM = "jsf_sequence";
112 
113     private static final String RENDER_KIT_IMPL = RendererUtils.class.getName()
114             + ".RenderKitImpl";
115 
116     // This nice constant is "specified" 13.1.1.2 The Resource API Approach in Spec as an example
117     public static final String RES_NOT_FOUND = "RES_NOT_FOUND";
118 
119     public static String getPathToComponent(UIComponent component)
120     {
121         StringBuilder buf = new StringBuilder();
122 
123         if (component == null)
124         {
125             buf.append("{Component-Path : ");
126             buf.append("[null]}");
127             return buf.toString();
128         }
129 
130         getPathToComponent(component, buf);
131 
132         buf.insert(0, "{Component-Path : ");
133         Object location = component.getAttributes().get(
134                 UIComponent.VIEW_LOCATION_KEY);
135         if (location != null)
136         {
137             buf.append(" Location: ").append(location);
138         }
139         buf.append("}");
140 
141         return buf.toString();
142     }
143 
144     private static void getPathToComponent(UIComponent component,
145             StringBuilder buf)
146     {
147         if (component == null)
148         {
149             return;
150         }
151 
152         StringBuilder intBuf = new StringBuilder();
153 
154         intBuf.append("[Class: ");
155         intBuf.append(component.getClass().getName());
156         if (component instanceof UIViewRoot)
157         {
158             intBuf.append(",ViewId: ");
159             intBuf.append(((UIViewRoot) component).getViewId());
160         }
161         else
162         {
163             intBuf.append(",Id: ");
164             intBuf.append(component.getId());
165         }
166         intBuf.append("]");
167 
168         buf.insert(0, intBuf.toString());
169 
170         getPathToComponent(component.getParent(), buf);
171     }
172 
173     public static String getConcatenatedId(FacesContext context,
174             UIComponent container, String clientId)
175     {
176         UIComponent child = container.findComponent(clientId);
177 
178         if (child == null)
179         {
180             return clientId;
181         }
182 
183         return getConcatenatedId(context, child);
184     }
185 
186     public static String getConcatenatedId(FacesContext context,
187             UIComponent component)
188     {
189         if (context == null)
190         {
191             throw new NullPointerException("context");
192         }
193 
194         StringBuilder idBuf = new StringBuilder();
195 
196         idBuf.append(component.getId());
197 
198         UIComponent parent;
199 
200         while ((parent = component.getParent()) != null)
201         {
202             if (parent instanceof NamingContainer)
203             {
204                 idBuf.insert(0, context.getNamingContainerSeparatorChar());
205                 idBuf.insert(0, parent.getId());
206             }
207         }
208 
209         return idBuf.toString();
210     }
211 
212     public static Boolean getBooleanValue(UIComponent component)
213     {
214         Object value = getObjectValue(component);
215         // Try to convert to Boolean if it is a String
216         if (value instanceof String)
217         {
218             value = Boolean.valueOf((String) value);
219         }
220 
221         if (value == null || value instanceof Boolean)
222         {
223             return (Boolean) value;
224         }
225 
226         throw new IllegalArgumentException(
227                 "Expected submitted value of type Boolean for Component : "
228                         + getPathToComponent(component));
229 
230     }
231 
232     public static Date getDateValue(UIComponent component)
233     {
234         Object value = getObjectValue(component);
235         if (value == null || value instanceof Date)
236         {
237             return (Date) value;
238         }
239 
240         throw new IllegalArgumentException(
241                 "Expected submitted value of type Date for component : "
242                         + getPathToComponent(component));
243     }
244 
245     public static Object getObjectValue(UIComponent component)
246     {
247         if (!(component instanceof ValueHolder))
248         {
249             throw new IllegalArgumentException("Component : "
250                     + getPathToComponent(component) + "is not a ValueHolder");
251         }
252 
253         if (component instanceof EditableValueHolder)
254         {
255             Object value = ((EditableValueHolder) component)
256                     .getSubmittedValue();
257             if (value != null)
258             {
259                 return value;
260             }
261         }
262 
263         return ((ValueHolder) component).getValue();
264     }
265 
266     @Deprecated
267     public static String getStringValue(FacesContext context, ValueBinding vb)
268     {
269         Object value = vb.getValue(context);
270         if (value != null)
271         {
272             return value.toString();
273         }
274         return null;
275     }
276 
277     public static String getStringValue(FacesContext context, ValueExpression ve)
278     {
279         Object value = ve.getValue(context.getELContext());
280         if (value != null)
281         {
282             return value.toString();
283         }
284         return null;
285     }
286 
287     public static String getStringValue(FacesContext facesContext,
288             UIComponent component)
289     {
290         if (!(component instanceof ValueHolder))
291         {
292             throw new IllegalArgumentException("Component : "
293                     + getPathToComponent(component)
294                     + "is not a ValueHolder");
295         }
296 
297         if (component instanceof EditableValueHolder)
298         {
299             Object submittedValue = ((EditableValueHolder) component)
300                     .getSubmittedValue();
301             if (submittedValue != null)
302             {
303                 if (log.isLoggable(Level.FINE))
304                 {
305                     log.fine("returning 1 '" + submittedValue + "'");
306                 }
307                 return submittedValue.toString();
308             }
309         }
310 
311         Object value;
312 
313         if (component instanceof EditableValueHolder)
314         {
315 
316             EditableValueHolder holder = (EditableValueHolder) component;
317 
318             if (holder.isLocalValueSet())
319             {
320                 value = holder.getLocalValue();
321             }
322             else
323             {
324                 value = getValue(component);
325             }
326         }
327         else
328         {
329             value = getValue(component);
330         }
331 
332         Converter converter = ((ValueHolder) component).getConverter();
333         if (converter == null && value != null)
334         {
335 
336             try
337             {
338                 converter = facesContext.getApplication().createConverter(
339                         value.getClass());
340                 if (log.isLoggable(Level.FINE))
341                 {
342                     log.fine("the created converter is " + converter);
343                 }
344             }
345             catch (FacesException e)
346             {
347                 log.log(Level.SEVERE, "No converter for class "
348                         + value.getClass().getName()
349                         + " found (component id=" + component.getId()
350                         + ").", e);
351                 // converter stays null
352             }
353         }
354 
355         if (converter == null)
356         {
357             if (value == null)
358             {
359                 if (log.isLoggable(Level.FINE))
360                 {
361                     log.fine("returning an empty string");
362                 }
363                 return "";
364             }
365 
366             if (log.isLoggable(Level.FINE))
367             {
368                 log.fine("returning an .toString");
369             }
370             return value.toString();
371 
372         }
373 
374         if (log.isLoggable(Level.FINE))
375         {
376             log.fine("returning converter get as string " + converter);
377         }
378         return converter.getAsString(facesContext, component, value);
379     }
380 
381     public static String getStringFromSubmittedValueOrLocalValueReturnNull(
382             FacesContext facesContext, UIComponent component)
383     {
384         try
385         {
386             if (!(component instanceof ValueHolder))
387             {
388                 throw new IllegalArgumentException("Component : "
389                         + getPathToComponent(component)
390                         + "is not a ValueHolder");
391             }
392 
393             if (component instanceof EditableValueHolder)
394             {
395                 Object submittedValue = ((EditableValueHolder) component)
396                         .getSubmittedValue();
397                 if (submittedValue != null)
398                 {
399                     if (log.isLoggable(Level.FINE))
400                     {
401                         log.fine("returning 1 '" + submittedValue + "'");
402                     }
403                     return submittedValue.toString();
404                 }
405             }
406 
407             Object value;
408 
409             if (component instanceof EditableValueHolder)
410             {
411 
412                 EditableValueHolder holder = (EditableValueHolder) component;
413 
414                 if (holder.isLocalValueSet())
415                 {
416                     value = holder.getLocalValue();
417                 }
418                 else
419                 {
420                     value = getValue(component);
421                 }
422             }
423             else
424             {
425                 value = getValue(component);
426             }
427 
428             Converter converter = ((ValueHolder) component).getConverter();
429             if (converter == null && value != null)
430             {
431 
432                 try
433                 {
434                     converter = facesContext.getApplication().createConverter(
435                             value.getClass());
436                     if (log.isLoggable(Level.FINE))
437                     {
438                         log.fine("the created converter is " + converter);
439                     }
440                 }
441                 catch (FacesException e)
442                 {
443                     log.log(Level.SEVERE, "No converter for class "
444                             + value.getClass().getName()
445                             + " found (component id=" + component.getId()
446                             + ").", e);
447                     // converter stays null
448                 }
449             }
450 
451             if (converter == null)
452             {
453                 if (value == null)
454                 {
455                     //if (log.isLoggable(Level.FINE))
456                     //    log.fine("returning an empty string");
457                     return null;
458                 }
459 
460                 if (log.isLoggable(Level.FINE))
461                 {
462                     log.fine("returning an .toString");
463                 }
464                 return value.toString();
465 
466             }
467 
468             if (log.isLoggable(Level.FINE))
469             {
470                 log.fine("returning converter get as string " + converter);
471             }
472             return converter.getAsString(facesContext, component, value);
473 
474         }
475         catch (PropertyNotFoundException ex)
476         {
477             log.log(Level.SEVERE, "Property not found - called by component : "
478                     + getPathToComponent(component), ex);
479 
480             throw ex;
481         }
482     }
483 
484     private static Object getValue(UIComponent component)
485     {
486         Object value = ((ValueHolder) component).getValue();
487         return value;
488     }
489 
490     /**
491      * See JSF Spec. 8.5 Table 8-1
492      * @param value
493      * @return boolean
494      */
495     public static boolean isDefaultAttributeValue(Object value)
496     {
497         if (value == null)
498         {
499             return true;
500         }
501         else if (value instanceof Boolean)
502         {
503             return !((Boolean) value).booleanValue();
504         }
505         else if (value instanceof Number)
506         {
507             if (value instanceof Integer)
508             {
509                 return ((Number) value).intValue() == Integer.MIN_VALUE;
510             }
511             else if (value instanceof Double)
512             {
513                 return ((Number) value).doubleValue() == Double.MIN_VALUE;
514             }
515             else if (value instanceof Long)
516             {
517                 return ((Number) value).longValue() == Long.MIN_VALUE;
518             }
519             else if (value instanceof Byte)
520             {
521                 return ((Number) value).byteValue() == Byte.MIN_VALUE;
522             }
523             else if (value instanceof Float)
524             {
525                 return ((Number) value).floatValue() == Float.MIN_VALUE;
526             }
527             else if (value instanceof Short)
528             {
529                 return ((Number) value).shortValue() == Short.MIN_VALUE;
530             }
531         }
532         return false;
533     }
534 
535     /**
536      * Find the proper Converter for the given UIOutput component.
537      * @return the Converter or null if no Converter specified or needed
538      * @throws FacesException if the Converter could not be created
539      */
540     public static Converter findUIOutputConverter(FacesContext facesContext,
541             UIOutput component) throws FacesException
542     {
543         return _SharedRendererUtils.findUIOutputConverter(facesContext,
544                 component);
545     }
546 
547     /**
548      * Calls findUISelectManyConverter with considerValueType = false.
549      * @param facesContext
550      * @param component
551      * @return
552      */
553     public static Converter findUISelectManyConverter(
554             FacesContext facesContext, UISelectMany component)
555     {
556         return findUISelectManyConverter(facesContext, component, false);
557     }
558 
559     /**
560      * Find proper Converter for the entries in the associated Collection or array of
561      * the given UISelectMany as specified in API Doc of UISelectMany.
562      * If considerValueType is true, the valueType attribute will be used
563      * in addition to the standard algorithm to get a valid converter.
564      * 
565      * @return the Converter or null if no Converter specified or needed
566      * @throws FacesException if the Converter could not be created
567      */
568     public static Converter findUISelectManyConverter(
569             FacesContext facesContext, UISelectMany component,
570             boolean considerValueType)
571     {
572         // If the component has an attached Converter, use it.
573         Converter converter = component.getConverter();
574         if (converter != null)
575         {
576             return converter;
577         }
578 
579         if (considerValueType)
580         {
581             // try to get a converter from the valueType attribute
582             converter = _SharedRendererUtils.getValueTypeConverter(
583                     facesContext, component);
584             if (converter != null)
585             {
586                 return converter;
587             }
588         }
589 
590         //Try to find out by value expression
591         ValueExpression ve = component.getValueExpression("value");
592         if (ve == null)
593         {
594             return null;
595         }
596 
597         // Try to get the type from the actual value or,
598         // if value == null, obtain the type from the ValueExpression
599         Class<?> valueType = null;
600         Object value = ve.getValue(facesContext.getELContext());
601         valueType = (value != null) ? value.getClass() : ve
602                 .getType(facesContext.getELContext());
603 
604         if (valueType == null)
605         {
606             return null;
607         }
608 
609         // a valueType of Object is also permitted, in order to support
610         // managed bean properties of type Object that resolve to null at this point
611         if (Collection.class.isAssignableFrom(valueType)
612                 || Object.class.equals(valueType))
613         {
614             // try to get the by-type-converter from the type of the SelectItems
615             return _SharedRendererUtils.getSelectItemsValueConverter(
616                     new SelectItemsIterator(component, facesContext),
617                     facesContext);
618         }
619 
620         if (!valueType.isArray())
621         {
622             throw new IllegalArgumentException(
623                     "ValueExpression for UISelectMany : "
624                             + getPathToComponent(component)
625                             + " must be of type Collection or Array");
626         }
627 
628         Class<?> arrayComponentType = valueType.getComponentType();
629         if (String.class.equals(arrayComponentType))
630         {
631             return null; //No converter needed for String type
632         }
633 
634         if (Object.class.equals(arrayComponentType))
635         {
636             // There is no converter for Object class
637             // try to get the by-type-converter from the type of the SelectItems
638             return _SharedRendererUtils.getSelectItemsValueConverter(
639                     new SelectItemsIterator(component, facesContext),
640                     facesContext);
641         }
642 
643         try
644         {
645             return facesContext.getApplication().createConverter(
646                     arrayComponentType);
647         }
648         catch (FacesException e)
649         {
650             log.log(Level.SEVERE,
651                     "No Converter for type " + arrayComponentType.getName()
652                             + " found", e);
653             return null;
654         }
655     }
656 
657     public static void checkParamValidity(FacesContext facesContext,
658             UIComponent uiComponent, Class compClass)
659     {
660         if (facesContext == null)
661         {
662             throw new NullPointerException("facesContext may not be null");
663         }
664         if (uiComponent == null)
665         {
666             throw new NullPointerException("uiComponent may not be null");
667         }
668 
669         //if (compClass != null && !(compClass.isAssignableFrom(uiComponent.getClass())))
670         // why isAssignableFrom with additional getClass method call if isInstance does the same?
671         if (compClass != null && !(compClass.isInstance(uiComponent)))
672         {
673             throw new IllegalArgumentException("uiComponent : "
674                     + getPathToComponent(uiComponent) + " is not instance of "
675                     + compClass.getName() + " as it should be");
676         }
677     }
678 
679     public static void renderChildren(FacesContext facesContext,
680             UIComponent component) throws IOException
681     {
682         if (component.getChildCount() > 0)
683         {
684             for (int i = 0; i < component.getChildCount(); i++)
685             {
686                 UIComponent child = component.getChildren().get(i);
687                 //renderChild(facesContext, child);
688                 child.encodeAll(facesContext);
689             }
690         }
691     }
692 
693     /**
694      * 
695      * @param facesContext
696      * @param child
697      * @throws IOException
698      * @deprecated use UIComponent.encodeAll() instead
699      */
700     @Deprecated
701     public static void renderChild(FacesContext facesContext, UIComponent child)
702             throws IOException
703     {
704         // The next isRendered() call is only shortcut:
705         // methods encodeBegin, encodeChildren and encodeEnd should proceed only if 
706         // "If our rendered property is true, render the (beginning, child, ending) of this component"
707         if (!isRendered(facesContext, child))
708         {
709             return;
710         }
711 
712         child.encodeBegin(facesContext);
713         if (child.getRendersChildren())
714         {
715             child.encodeChildren(facesContext);
716         }
717         else
718         {
719             renderChildren(facesContext, child);
720         }
721         child.encodeEnd(facesContext);
722     }
723 
724     /**
725      * Call {@link #pushComponentToEL(javax.faces.context.FacesContext,javax.faces.component.UIComponent)}, 
726      * reads the isRendered property, call {@link
727      * UIComponent#popComponentFromEL} and returns the value of isRendered.
728      */
729     public static boolean isRendered(FacesContext facesContext,
730             UIComponent uiComponent)
731     {
732         // We must call pushComponentToEL here because ValueExpression may have 
733         // implicit object "component" used. 
734         try
735         {
736             uiComponent.pushComponentToEL(facesContext, uiComponent);
737             return uiComponent.isRendered();
738         }
739         finally
740         {
741             uiComponent.popComponentFromEL(facesContext);
742         }
743     }
744 
745     public static List getSelectItemList(UISelectOne uiSelectOne)
746     {
747         return internalGetSelectItemList(uiSelectOne,
748                 FacesContext.getCurrentInstance());
749     }
750 
751     /**
752      * @param uiSelectOne
753      * @param facesContext
754      * @return List of SelectItem Objects
755      */
756     public static List getSelectItemList(UISelectOne uiSelectOne,
757             FacesContext facesContext)
758     {
759         return internalGetSelectItemList(uiSelectOne, facesContext);
760     }
761 
762     public static List getSelectItemList(UISelectMany uiSelectMany)
763     {
764         return internalGetSelectItemList(uiSelectMany,
765                 FacesContext.getCurrentInstance());
766     }
767 
768     /**
769      * @param uiSelectMany
770      * @param facesContext
771      * @return List of SelectItem Objects
772      */
773     public static List getSelectItemList(UISelectMany uiSelectMany,
774             FacesContext facesContext)
775     {
776         return internalGetSelectItemList(uiSelectMany, facesContext);
777     }
778 
779     private static List internalGetSelectItemList(UIComponent uiComponent,
780             FacesContext facesContext)
781     {
782         /* TODO: Shall we cache the list in a component attribute?
783         ArrayList list = (ArrayList)uiComponent.getAttributes().get(SELECT_ITEM_LIST_ATTR);
784         if (list != null)
785         {
786             return list;
787         }
788          */
789 
790         List list = new ArrayList();
791 
792         for (Iterator iter = new SelectItemsIterator(uiComponent, facesContext); iter
793                 .hasNext();)
794         {
795             list.add(iter.next());
796         }
797         return list;
798     }
799 
800     /**
801      * Convenient utility method that returns the currently submitted values of
802      * a UISelectMany component as a Set, of which the contains method can then be
803      * easily used to determine if a select item is currently selected.
804      * Calling the contains method of this Set with the renderable (String converted) item value
805      * as argument returns true if this item is selected.
806      * @param uiSelectMany
807      * @return Set containing all currently selected values
808      */
809     public static Set getSubmittedValuesAsSet(FacesContext context,
810             UIComponent component, Converter converter,
811             UISelectMany uiSelectMany)
812     {
813         Object submittedValues = uiSelectMany.getSubmittedValue();
814         if (submittedValues == null)
815         {
816             return null;
817         }
818 
819         if (converter != null)
820         {
821             converter = new PassThroughAsStringConverter(converter);
822         }
823 
824         return internalSubmittedOrSelectedValuesAsSet(context, component,
825                 converter, uiSelectMany, submittedValues, false);
826     }
827 
828     /**
829      * Convenient utility method that returns the currently selected values of
830      * a UISelectMany component as a Set, of which the contains method can then be
831      * easily used to determine if a value is currently selected.
832      * Calling the contains method of this Set with the item value
833      * as argument returns true if this item is selected.
834      * @param uiSelectMany
835      * @return Set containing all currently selected values
836      */
837     public static Set getSelectedValuesAsSet(FacesContext context,
838             UIComponent component, Converter converter,
839             UISelectMany uiSelectMany)
840     {
841         Object selectedValues = uiSelectMany.getValue();
842 
843         return internalSubmittedOrSelectedValuesAsSet(context, component,
844                 converter, uiSelectMany, selectedValues, true);
845     }
846 
847     /**
848      * Convenient utility method that returns the currently given value as String,
849      * using the given converter.
850      * Especially usefull for dealing with primitive types.
851      */
852     public static String getConvertedStringValue(FacesContext context,
853             UIComponent component, Converter converter, Object value)
854     {
855         if (converter == null)
856         {
857             if (value == null)
858             {
859                 return "";
860             }
861             else if (value instanceof String)
862             {
863                 return (String) value;
864             }
865             else
866             {
867                 return value.toString();
868             }
869         }
870 
871         return converter.getAsString(context, component, value);
872     }
873 
874     /**
875      * Convenient utility method that returns the currently given SelectItem value
876      * as String, using the given converter.
877      * Especially usefull for dealing with primitive types.
878      */
879     public static String getConvertedStringValue(FacesContext context,
880             UIComponent component, Converter converter, SelectItem selectItem)
881     {
882         return getConvertedStringValue(context, component, converter,
883                 selectItem.getValue());
884     }
885 
886     private static Set internalSubmittedOrSelectedValuesAsSet(
887             FacesContext context, UIComponent component, Converter converter,
888             UISelectMany uiSelectMany, Object values,
889             boolean allowNonArrayOrCollectionValue)
890     {
891         if (values == null || EMPTY_STRING.equals(values))
892         {
893             return Collections.EMPTY_SET;
894         }
895         else if (values instanceof Object[])
896         {
897             //Object array
898             Object[] ar = (Object[]) values;
899             if (ar.length == 0)
900             {
901                 return Collections.EMPTY_SET;
902             }
903 
904             HashSet set = new HashSet(HashMapUtils.calcCapacity(ar.length));
905             for (int i = 0; i < ar.length; i++)
906             {
907                 set.add(getConvertedStringValue(context, component, converter,
908                         ar[i]));
909             }
910             return set;
911         }
912         else if (values.getClass().isArray())
913         {
914             //primitive array
915             int len = Array.getLength(values);
916             HashSet set = new HashSet(
917                     org.apache.myfaces.shared.util.HashMapUtils
918                             .calcCapacity(len));
919             for (int i = 0; i < len; i++)
920             {
921                 set.add(getConvertedStringValue(context, component, converter,
922                         Array.get(values, i)));
923             }
924             return set;
925         }
926         else if (values instanceof Collection)
927         {
928             Collection col = (Collection) values;
929             if (col.size() == 0)
930             {
931                 return Collections.EMPTY_SET;
932             }
933 
934             HashSet set = new HashSet(HashMapUtils.calcCapacity(col.size()));
935             for (Iterator i = col.iterator(); i.hasNext();)
936             {
937                 set.add(getConvertedStringValue(context, component, converter,
938                         i.next()));
939             }
940 
941             return set;
942 
943         }
944         else if (allowNonArrayOrCollectionValue)
945         {
946             HashSet set = new HashSet(HashMapUtils.calcCapacity(1));
947             set.add(values);
948             return set;
949         }
950         else
951         {
952             throw new IllegalArgumentException(
953                     "Value of UISelectMany component with path : "
954                             + getPathToComponent(uiSelectMany)
955                             + " is not of type Array or List");
956         }
957     }
958 
959     public static Object getConvertedUISelectOneValue(
960             FacesContext facesContext, UISelectOne output, Object submittedValue)
961     {
962         if (submittedValue != null && !(submittedValue instanceof String))
963         {
964             throw new IllegalArgumentException(
965                     "Submitted value of type String for component : "
966                             + getPathToComponent(output) + "expected");
967         }
968 
969         //To be compatible with jsf ri, and according to issue 69
970         //[  Permit the passing of a null value to SelectItem.setValue()  ]
971         //If submittedValue == "" then convert to null.
972         if ((submittedValue != null)
973                 && ("".equals(submittedValue)))
974         {
975             //Replace "" by null value
976             submittedValue = null;
977         }
978 
979         Converter converter;
980         try
981         {
982             converter = findUIOutputConverter(facesContext, output);
983         }
984         catch (FacesException e)
985         {
986             throw new ConverterException(e);
987         }
988 
989         return converter == null ? submittedValue : converter.getAsObject(
990                 facesContext, output, (String) submittedValue);
991     }
992 
993     public static Object getConvertedUIOutputValue(FacesContext facesContext,
994             UIOutput output, Object submittedValue) throws ConverterException
995     {
996         if (submittedValue != null && !(submittedValue instanceof String))
997         {
998             submittedValue = submittedValue.toString();
999         }
1000 
1001         Converter converter;
1002         try
1003         {
1004             converter = findUIOutputConverter(facesContext, output);
1005         }
1006         catch (FacesException e)
1007         {
1008             throw new ConverterException(e);
1009         }
1010 
1011         return converter == null ? submittedValue : converter.getAsObject(
1012                 facesContext, output, (String) submittedValue);
1013     }
1014 
1015     /**
1016      * Invokes getConvertedUISelectManyValue() with considerValueType = false, thus
1017      * implementing the standard behavior of the spec (valueType comes from Tomahawk).
1018      * 
1019      * @param facesContext
1020      * @param selectMany
1021      * @param submittedValue
1022      * @return
1023      * @throws ConverterException
1024      */
1025     public static Object getConvertedUISelectManyValue(
1026             FacesContext facesContext, UISelectMany selectMany,
1027             Object submittedValue) throws ConverterException
1028     {
1029         // do not consider the valueType attribute
1030         return getConvertedUISelectManyValue(facesContext, selectMany,
1031                 submittedValue, false);
1032     }
1033 
1034     /**
1035      * Gets the converted value of a UISelectMany component.
1036      * 
1037      * @param facesContext
1038      * @param selectMany
1039      * @param submittedValue
1040      * @param considerValueType if true, the valueType attribute of the component will
1041      *                          also be used (applies for Tomahawk UISelectMany components)
1042      * @return
1043      * @throws ConverterException
1044      */
1045     public static Object getConvertedUISelectManyValue(
1046             FacesContext facesContext, UISelectMany selectMany,
1047             Object submittedValue, boolean considerValueType)
1048             throws ConverterException
1049     {
1050         if (submittedValue == null)
1051         {
1052             return null;
1053         }
1054 
1055         if (!(submittedValue instanceof String[]))
1056         {
1057             throw new ConverterException(
1058                     "Submitted value of type String[] for component : "
1059                             + getPathToComponent(selectMany) + "expected");
1060         }
1061 
1062         return _SharedRendererUtils.getConvertedUISelectManyValue(facesContext,
1063                 selectMany, (String[]) submittedValue, considerValueType);
1064     }
1065 
1066     public static boolean getBooleanAttribute(UIComponent component,
1067             String attrName, boolean defaultValue)
1068     {
1069         Boolean b = (Boolean) component.getAttributes().get(attrName);
1070         return b != null ? b.booleanValue() : defaultValue;
1071     }
1072 
1073     public static int getIntegerAttribute(UIComponent component,
1074             String attrName, int defaultValue)
1075     {
1076         Integer i = (Integer) component.getAttributes().get(attrName);
1077         return i != null ? i.intValue() : defaultValue;
1078     }
1079 
1080     private static final String TRINIDAD_FORM_COMPONENT_FAMILY = "org.apache.myfaces.trinidad.Form";
1081     private static final String ADF_FORM_COMPONENT_FAMILY = "oracle.adf.Form";
1082 
1083     /**
1084      * Find the enclosing form of a component
1085      * in the view-tree.
1086      * All Subclasses of <code>UIForm</code> and all known
1087      * form-families are searched for.
1088      * Currently those are the Trinidad form family,
1089      * and the (old) ADF Faces form family.
1090      * <p/>
1091      * There might be additional form families
1092      * which have to be explicitly entered here.
1093      *
1094      * @param uiComponent
1095      * @param facesContext
1096      * @return FormInfo Information about the form - the form itself and its name.
1097      */
1098     public static FormInfo findNestingForm(UIComponent uiComponent,
1099             FacesContext facesContext)
1100     {
1101         UIComponent parent = uiComponent.getParent();
1102         while (parent != null
1103                 && (!ADF_FORM_COMPONENT_FAMILY.equals(parent.getFamily())
1104                         && !TRINIDAD_FORM_COMPONENT_FAMILY.equals(parent
1105                                 .getFamily()) && !(parent instanceof UIForm)))
1106         {
1107             parent = parent.getParent();
1108         }
1109 
1110         if (parent != null)
1111         {
1112             //link is nested inside a form
1113             String formName = parent.getClientId(facesContext);
1114             return new FormInfo(parent, formName);
1115         }
1116 
1117         return null;
1118     }
1119 
1120     public static boolean getBooleanValue(String attribute, Object value,
1121             boolean defaultValue)
1122     {
1123         if (value instanceof Boolean)
1124         {
1125             return ((Boolean) value).booleanValue();
1126         }
1127         else if (value instanceof String)
1128         {
1129             return Boolean.valueOf((String) value).booleanValue();
1130         }
1131         else if (value != null)
1132         {
1133             log.severe("value for attribute "
1134                     + attribute
1135                     + " must be instanceof 'Boolean' or 'String', is of type : "
1136                     + value.getClass());
1137 
1138             return defaultValue;
1139         }
1140 
1141         return defaultValue;
1142     }
1143 
1144     public static void copyHtmlInputTextAttributes(HtmlInputText src,
1145             HtmlInputText dest)
1146     {
1147         dest.setId(src.getId());
1148         boolean forceId = getBooleanValue(JSFAttr.FORCE_ID_ATTR, src
1149                 .getAttributes().get(JSFAttr.FORCE_ID_ATTR), false);
1150         if (forceId)
1151         {
1152             dest.getAttributes().put(JSFAttr.FORCE_ID_ATTR, Boolean.TRUE);
1153         }
1154         dest.setImmediate(src.isImmediate());
1155         dest.setTransient(src.isTransient());
1156         dest.setAccesskey(src.getAccesskey());
1157         dest.setAlt(src.getAlt());
1158         dest.setConverter(src.getConverter());
1159         dest.setDir(src.getDir());
1160         dest.setDisabled(src.isDisabled());
1161         dest.setLang(src.getLang());
1162         dest.setLocalValueSet(src.isLocalValueSet());
1163         dest.setMaxlength(src.getMaxlength());
1164         dest.setOnblur(src.getOnblur());
1165         dest.setOnchange(src.getOnchange());
1166         dest.setOnclick(src.getOnclick());
1167         dest.setOndblclick(src.getOndblclick());
1168         dest.setOnfocus(src.getOnfocus());
1169         dest.setOnkeydown(src.getOnkeydown());
1170         dest.setOnkeypress(src.getOnkeypress());
1171         dest.setOnkeyup(src.getOnkeyup());
1172         dest.setOnmousedown(src.getOnmousedown());
1173         dest.setOnmousemove(src.getOnmousemove());
1174         dest.setOnmouseout(src.getOnmouseout());
1175         dest.setOnmouseover(src.getOnmouseover());
1176         dest.setOnmouseup(src.getOnmouseup());
1177         dest.setOnselect(src.getOnselect());
1178         dest.setReadonly(src.isReadonly());
1179         dest.setRendered(src.isRendered());
1180         dest.setRequired(src.isRequired());
1181         dest.setSize(src.getSize());
1182         dest.setStyle(src.getStyle());
1183         dest.setStyleClass(src.getStyleClass());
1184         dest.setTabindex(src.getTabindex());
1185         dest.setTitle(src.getTitle());
1186         dest.setValidator(src.getValidator());
1187     }
1188 
1189     public static UIComponent findComponent(UIComponent headerComp, Class clazz)
1190     {
1191         if (clazz.isAssignableFrom(headerComp.getClass()))
1192         {
1193             return headerComp;
1194         }
1195 
1196         List li = headerComp.getChildren();
1197 
1198         for (int i = 0; i < li.size(); i++)
1199         {
1200             UIComponent comp = (UIComponent) li.get(i);
1201 
1202             //recursively iterate through children to find the component
1203             UIComponent lookupComp = findComponent(comp, clazz);
1204 
1205             if (lookupComp != null)
1206             {
1207                 return lookupComp;
1208             }
1209         }
1210 
1211         return null;
1212     }
1213 
1214     public static void addOrReplaceChild(UIInput component, UIComponent child)
1215     {
1216         List li = component.getChildren();
1217 
1218         for (int i = 0; i < li.size(); i++)
1219         {
1220             UIComponent oldChild = (UIComponent) li.get(i);
1221 
1222             if (oldChild.getId() != null
1223                     && oldChild.getId().equals(child.getId()))
1224             {
1225                 li.set(i, child);
1226                 return;
1227             }
1228         }
1229 
1230         component.getChildren().add(child);
1231     }
1232 
1233     public static String getClientId(FacesContext facesContext,
1234             UIComponent uiComponent, String forAttr)
1235     {
1236         UIComponent forComponent = uiComponent.findComponent(forAttr);
1237         if (forComponent == null)
1238         {
1239             final char separatorChar = facesContext.getNamingContainerSeparatorChar();
1240             
1241             Level level = Level.WARNING;
1242             boolean productionStage = facesContext.isProjectStage(ProjectStage.Production);
1243             if (productionStage)
1244             {
1245                 level = Level.FINE;
1246             }
1247             if (log.isLoggable(level))
1248             {
1249                 StringBuilder sb = new StringBuilder();
1250                 sb.append("Unable to find component '");
1251                 sb.append(forAttr);
1252                 sb.append("' (calling findComponent on component '");
1253                 sb.append(uiComponent.getClientId(facesContext));
1254                 sb.append("'");
1255                 if (!productionStage)
1256                 {
1257                     sb.append(", viewLocation: ");
1258                     sb.append(uiComponent.getAttributes().get(UIComponent.VIEW_LOCATION_KEY));
1259                 }
1260                 sb.append(").");
1261                 sb.append(" We'll try to return a guessed client-id anyways -");
1262                 sb.append(" this will be a problem if you put the referenced component");
1263                 sb.append(" into a different naming-container. If this is the case, ");
1264                 sb.append("you can always use the full client-id.");
1265                 log.info(sb.toString());
1266             }
1267             if (forAttr.length() > 0 && forAttr.charAt(0) == separatorChar)
1268             {
1269                 //absolute id path
1270                 return forAttr.substring(1);
1271             }
1272 
1273             //relative id path, we assume a component on the same level as the label component
1274             String labelClientId = uiComponent.getClientId(facesContext);
1275             int colon = labelClientId.lastIndexOf(separatorChar);
1276 
1277             return colon == -1 ? forAttr : labelClientId
1278                     .substring(0, colon + 1) + forAttr;
1279 
1280         }
1281 
1282         return forComponent.getClientId(facesContext);
1283 
1284     }
1285 
1286     public static List convertIdsToClientIds(String actionFor,
1287             FacesContext facesContext, UIComponent component)
1288     {
1289         List li = new ArrayList();
1290 
1291         String[] ids = actionFor.split(",");
1292 
1293         for (int i = 0; i < ids.length; i++)
1294         {
1295             String trimedId = ids[i].trim();
1296             if (trimedId.equals("none"))
1297             {
1298                 li.add(trimedId);
1299             }
1300             else
1301             {
1302                 li.add(RendererUtils.getClientId(facesContext, component,
1303                         trimedId));
1304             }
1305         }
1306         return li;
1307     }
1308 
1309     public static List convertPhasesToPhasesIds(String actionForPhase)
1310     {
1311         List li = new ArrayList();
1312 
1313         if (actionForPhase == null)
1314         {
1315             return li;
1316         }
1317 
1318         String[] ids = actionForPhase.split(",");
1319 
1320         for (int i = 0; i < ids.length; i++)
1321         {
1322             if (ids[i].equals("PROCESS_VALIDATIONS"))
1323             {
1324                 li.add(PhaseId.PROCESS_VALIDATIONS);
1325             }
1326             else if (ids[i].equals("UPDATE_MODEL_VALUES"))
1327             {
1328                 li.add(PhaseId.UPDATE_MODEL_VALUES);
1329             }
1330         }
1331         return li;
1332     }
1333 
1334     /**
1335      * Helper method which loads a resource file (such as css) by a given context path and a file name.
1336      * Useful to provide css files (or js files) inline.
1337      * 
1338      * @param ctx <code>FacesContext</code> object to calculate the context path of the web application.
1339      * @param file name of the resource file (e.g. <code>foo.css</code>).
1340      * @return the content of the resource file, or <code>null</code> if no such file is available.
1341      */
1342     public static String loadResourceFile(FacesContext ctx, String file)
1343     {
1344 
1345         ByteArrayOutputStream content = new ByteArrayOutputStream(10240);
1346 
1347         InputStream in = null;
1348         try
1349         {
1350             in = ctx.getExternalContext().getResourceAsStream(file);
1351             if (in == null)
1352             {
1353                 return null;
1354             }
1355 
1356             byte[] fileBuffer = new byte[10240];
1357             int read;
1358             while ((read = in.read(fileBuffer)) > -1)
1359             {
1360                 content.write(fileBuffer, 0, read);
1361             }
1362         }
1363         catch (FileNotFoundException e)
1364         {
1365             if (log.isLoggable(Level.WARNING))
1366             {
1367                 log.log(Level.WARNING, "no such file " + file, e);
1368             }
1369             content = null;
1370         }
1371         catch (IOException e)
1372         {
1373             if (log.isLoggable(Level.WARNING))
1374             {
1375                 log.log(Level.WARNING, "problems during processing resource "
1376                         + file, e);
1377             }
1378             content = null;
1379         }
1380         finally
1381         {
1382             try
1383             {
1384                 if (content != null)
1385                 {
1386                     content.close();
1387                 }
1388             }
1389             catch (IOException e)
1390             {
1391                 log.log(Level.WARNING, e.getLocalizedMessage(), e);
1392             }
1393             if (in != null)
1394             {
1395                 try
1396                 {
1397                     in.close();
1398                 }
1399                 catch (IOException e)
1400                 {
1401                     log.log(Level.WARNING, e.getLocalizedMessage(), e);
1402                 }
1403             }
1404         }
1405 
1406         return content != null ? content.toString() : null;
1407     }
1408 
1409     /**
1410      * check for partial validation or model update attributes being set
1411      * and initialize the request-map accordingly.
1412      * SubForms will work with this information.
1413      */
1414     public static void initPartialValidationAndModelUpdate(
1415             UIComponent component, FacesContext facesContext)
1416     {
1417         String actionFor = (String) component.getAttributes().get("actionFor");
1418 
1419         if (actionFor != null)
1420         {
1421             List li = convertIdsToClientIds(actionFor, facesContext, component);
1422 
1423             facesContext.getExternalContext().getRequestMap()
1424                     .put(ACTION_FOR_LIST, li);
1425 
1426             String actionForPhase = (String) component.getAttributes().get(
1427                     "actionForPhase");
1428 
1429             if (actionForPhase != null)
1430             {
1431                 List phaseList = convertPhasesToPhasesIds(actionForPhase);
1432 
1433                 facesContext.getExternalContext().getRequestMap()
1434                         .put(ACTION_FOR_PHASE_LIST, phaseList);
1435             }
1436         }
1437     }
1438 
1439     public static boolean isAdfOrTrinidadForm(UIComponent component)
1440     {
1441         if (component == null)
1442         {
1443             return false;
1444         }
1445         return ADF_FORM_COMPONENT_FAMILY.equals(component.getFamily())
1446                 || TRINIDAD_FORM_COMPONENT_FAMILY.equals(component.getFamily());
1447     }
1448 
1449     /**
1450      * Gets the ResponseStateManager for the renderKit Id provided
1451      * 
1452      * @deprecated use FacesContext.getRenderKit() or getRenderKitFactory().getRenderKit(
1453      *               context, renderKitId).getResponseStateManager()
1454      */
1455     @Deprecated
1456     public static ResponseStateManager getResponseStateManager(
1457             FacesContext facesContext, String renderKitId)
1458             throws FacesException
1459     {
1460         RenderKit renderKit = facesContext.getRenderKit();
1461 
1462         if (renderKit == null)
1463         {
1464             // look for the renderkit in the request
1465             Map attributesMap = facesContext.getAttributes();
1466             RenderKitFactory factory = (RenderKitFactory) attributesMap
1467                     .get(RENDER_KIT_IMPL);
1468 
1469             if (factory != null)
1470             {
1471                 renderKit = factory.getRenderKit(facesContext, renderKitId);
1472             }
1473             else
1474             {
1475                 factory = (RenderKitFactory) FactoryFinder
1476                         .getFactory(FactoryFinder.RENDER_KIT_FACTORY);
1477 
1478                 if (factory == null)
1479                 {
1480                     throw new IllegalStateException("Factory is null");
1481                 }
1482 
1483                 attributesMap.put(RENDER_KIT_IMPL, factory);
1484 
1485                 renderKit = factory.getRenderKit(facesContext, renderKitId);
1486             }
1487         }
1488 
1489         if (renderKit == null)
1490         {
1491             throw new IllegalArgumentException(
1492                     "Could not find a RenderKit for \"" + renderKitId + "\"");
1493         }
1494 
1495         return renderKit.getResponseStateManager();
1496     }
1497 
1498     /**
1499       * Checks for name/library attributes on component and if they are avaliable,
1500       * creates {@link Resource} and returns it's path suitable for rendering.
1501       * If component doesn't have name/library gets value for attribute named <code>attributeName</code> 
1502       * returns it processed with {@link CoreRenderer#toResourceUri(FacesContext, Object)}
1503       *       
1504       * @param facesContext a {@link FacesContext}
1505       * @param component a {@link UIComponent}
1506       * @param attributeName name of attribute that represents "image", "icon", "source", ... 
1507       * 
1508       * @since 4.0.1
1509       */
1510     public static String getIconSrc(final FacesContext facesContext,
1511             final UIComponent component, final String attributeName)
1512     {
1513 
1514         // JSF 2.0: if "name" attribute is available, treat as a resource reference.
1515         final Map<String, Object> attributes = component.getAttributes();
1516         final String resourceName = (String) attributes.get(JSFAttr.NAME_ATTR);
1517         if (resourceName != null && (resourceName.length() > 0))
1518         {
1519 
1520             final ResourceHandler resourceHandler = facesContext
1521                     .getApplication().getResourceHandler();
1522             final Resource resource;
1523 
1524             final String libraryName = (String) component.getAttributes().get(
1525                     JSFAttr.LIBRARY_ATTR);
1526             if ((libraryName != null) && (libraryName.length() > 0))
1527             {
1528                 resource = resourceHandler.createResource(resourceName,
1529                         libraryName);
1530             }
1531             else
1532             {
1533                 resource = resourceHandler.createResource(resourceName);
1534             }
1535 
1536             if (resource == null)
1537             {
1538                 // If resourceName/libraryName are set but no resource created -> probably a typo,
1539                 // show a message
1540                 if (facesContext.isProjectStage(ProjectStage.Development))
1541                 {
1542                     String summary = "Unable to find resource: " + resourceName;
1543                     if (libraryName != null)
1544                     {
1545                         summary = summary + " from library: " + libraryName;
1546                     }
1547                     facesContext.addMessage(
1548                             component.getClientId(facesContext),
1549                             new FacesMessage(FacesMessage.SEVERITY_WARN,
1550                                     summary, summary));
1551                 }
1552 
1553                 return RES_NOT_FOUND;
1554             }
1555             else
1556             {
1557                 return resource.getRequestPath();
1558             }
1559         }
1560         else
1561         {
1562             String value = (String) component.getAttributes()
1563                     .get(attributeName);
1564             return toResourceUri(facesContext, value);
1565         }
1566     }
1567 
1568     /**
1569      * Coerces an object into a resource URI, calling the view-handler.
1570      */
1571     static public String toResourceUri(FacesContext facesContext, Object o)
1572     {
1573         if (o == null)
1574         {
1575             return null;
1576         }
1577 
1578         String uri = o.toString();
1579 
1580         // *** EL Coercion problem ***
1581         // If icon or image attribute was declared with #{resource[]} and that expression
1582         // evaluates to null (it means ResourceHandler.createResource returns null because 
1583         // requested resource does not exist)
1584         // EL implementation turns null into ""
1585         // see http://www.irian.at/blog/blogid/unifiedElCoercion/#unifiedElCoercion
1586         if (uri.length() == 0)
1587         {
1588             return null;
1589         }
1590 
1591         // With JSF 2.0 url for resources can be done with EL like #{resource['resourcename']}
1592         // and such EL after evalution contains context path for the current web application already,
1593         // -> we dont want call viewHandler.getResourceURL()
1594         if (uri.contains(ResourceHandler.RESOURCE_IDENTIFIER))
1595         {
1596             return uri;
1597         }
1598 
1599         // Treat two slashes as server-relative
1600         if (uri.startsWith("//"))
1601         {
1602             return uri.substring(1);
1603         }
1604         else
1605         {
1606             // If the specified path starts with a "/",
1607             // following method will prefix it with the context path for the current web application,
1608             // and return the result
1609             String resourceURL = facesContext.getApplication().getViewHandler()
1610                     .getResourceURL(facesContext, uri);
1611             return facesContext.getExternalContext().encodeResourceURL(
1612                     resourceURL);
1613         }
1614     }
1615 
1616     /**
1617      * Special converter for handling submitted values which don't need to be converted.
1618      */
1619     private static class PassThroughAsStringConverter implements Converter
1620     {
1621         private final Converter converter;
1622 
1623         public PassThroughAsStringConverter(Converter converter)
1624         {
1625             this.converter = converter;
1626         }
1627 
1628         public Object getAsObject(FacesContext context, UIComponent component,
1629                 String value) throws ConverterException
1630         {
1631             return converter.getAsObject(context, component, value);
1632         }
1633 
1634         public String getAsString(FacesContext context, UIComponent component,
1635                 Object value) throws ConverterException
1636         {
1637             return (String) value;
1638         }
1639 
1640     }
1641 }