1 /*
2 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
3 *
4 * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
5 *
6 * The contents of this file are subject to the terms of either the GNU
7 * General Public License Version 2 only ("GPL") or the Common Development
8 * and Distribution License("CDDL") (collectively, the "License"). You
9 * may not use this file except in compliance with the License. You can obtain
10 * a copy of the License at https://glassfish.dev.java.net/public/CDDL+GPL.html
11 * or glassfish/bootstrap/legal/LICENSE.txt. See the License for the specific
12 * language governing permissions and limitations under the License.
13 *
14 * When distributing the software, include this License Header Notice in each
15 * file and include the License file at glassfish/bootstrap/legal/LICENSE.txt.
16 * Sun designates this particular file as subject to the "Classpath" exception
17 * as provided by Sun in the GPL Version 2 section of the License file that
18 * accompanied this code. If applicable, add the following below the License
19 * Header, with the fields enclosed by brackets [] replaced by your own
20 * identifying information: "Portions Copyrighted [year]
21 * [name of copyright owner]"
22 *
23 * Contributor(s):
24 *
25 * If you wish your version of this file to be governed by only the CDDL or
26 * only the GPL Version 2, indicate your decision by adding "[Contributor]
27 * elects to include this software in this distribution under the [CDDL or GPL
28 * Version 2] license." If you don't indicate a single choice of license, a
29 * recipient has the option to distribute your version of this file under
30 * either the CDDL, the GPL Version 2 or to extend the choice of license to
31 * its licensees as provided above. However, if you add GPL Version 2 code
32 * and therefore, elected the GPL Version 2 license, then the option applies
33 * only if the new code is made subject to such option by the copyright
34 * holder.
35 */
36
37 package com.sun.faces.renderkit;
38
39 import java.io.BufferedReader;
40 import java.io.IOException;
41 import java.io.InputStream;
42 import java.io.InputStreamReader;
43 import java.io.StringReader;
44 import java.io.StringWriter;
45 import java.io.Writer;
46 import java.net.URL;
47 import java.net.URLConnection;
48 import java.util.ArrayList;
49 import java.util.Arrays;
50 import java.util.Collection;
51 import java.util.List;
52 import java.util.Map;
53 import java.util.Map.Entry;
54 import java.util.logging.Level;
55 import java.util.logging.Logger;
56 import java.math.BigDecimal;
57
58 import javax.faces.FacesException;
59 import javax.faces.FactoryFinder;
60 import javax.faces.component.UIComponent;
61 import javax.faces.component.UIComponentBase;
62 import javax.faces.component.UISelectItem;
63 import javax.faces.component.UISelectItems;
64 import javax.faces.context.ExternalContext;
65 import javax.faces.context.FacesContext;
66 import javax.faces.context.ResponseWriter;
67 import javax.faces.model.SelectItem;
68 import javax.faces.render.RenderKit;
69 import javax.faces.render.RenderKitFactory;
70 import javax.faces.render.ResponseStateManager;
71
72 import com.sun.faces.RIConstants;
73 import com.sun.faces.config.WebConfiguration;
74 import static com.sun.faces.config.WebConfiguration.BooleanWebContextInitParameter;
75 import com.sun.faces.renderkit.html_basic.HtmlBasicRenderer.Param;
76 import com.sun.faces.util.FacesLogger;
77 import com.sun.faces.util.MessageUtils;
78 import com.sun.faces.util.Util;
79 import com.sun.faces.util.RequestStateManager;
80
81 /**
82 * <p>A set of utilities for use in {@link RenderKit}s.</p>
83 */
84 public class RenderKitUtils {
85
86 /**
87 * <p>The prefix to append to certain attributes when renderking
88 * <code>XHTML Transitional</code> content.
89 */
90 private static final String XHTML_ATTR_PREFIX = "xml:";
91
92
93 /**
94 * <p><code>Boolean</code> attributes to be rendered
95 * using <code>XHMTL</code> semantics.
96 */
97 private static final String[] BOOLEAN_ATTRIBUTES = {
98 "disabled", "ismap", "readonly"
99 };
100
101
102 /**
103 * <p>An array of attributes that must be prefixed by
104 * {@link #XHTML_ATTR_PREFIX} when rendering
105 * <code>XHTML Transitional</code> content.
106 */
107 private static final String[] XHTML_PREFIX_ATTRIBUTES = {
108 "lang"
109 };
110
111 /**
112 * <p>The maximum number of array elements that can be used
113 * to hold content types from an accept String.</p>
114 */
115 private final static int MAX_CONTENT_TYPES = 50;
116
117 /**
118 * <p>The maximum number of content type parts.
119 * For example: for the type: "text/html; level=1; q=0.5"
120 * The parts of this type would be:
121 * "text" - type
122 * "html; level=1" - subtype
123 * "0.5" - quality value
124 * "1" - level value </p>
125 */
126 private final static int MAX_CONTENT_TYPE_PARTS = 4;
127
128 /**
129 * The character that is used to delimit content types
130 * in an accept String.</p>
131 */
132 private final static String CONTENT_TYPE_DELIMITER = ",";
133
134 /**
135 * The character that is used to delimit the type and
136 * subtype portions of a content type in an accept String.
137 * Example: text/html </p>
138 */
139 private final static String CONTENT_TYPE_SUBTYPE_DELIMITER = "/";
140
141 /**
142 * <p>JavaScript to be rendered when a commandLink is used.
143 * This may be expaned to include other uses.</p>
144 */
145 private static final String SUN_JSF_JS = RIConstants.FACES_PREFIX + "sunJsfJs";
146
147
148 /**
149 * This array represents the packages that can leverage the
150 * <code>attributesThatAreSet</code> List for optimized attribute
151 * rendering.
152 *
153 * IMPLEMENTATION NOTE: This must be kept in sync with the array
154 * in UIComponentBase$AttributesMap and HtmlComponentGenerator.
155 *
156 * Hopefully JSF 2.0 will remove the need for this.
157 */
158 private static final String[] OPTIMIZED_PACKAGES = {
159 "javax.faces.component",
160 "javax.faces.component.html"
161 };
162 static {
163 // Sort the array for use with Arrays.binarySearch()
164 Arrays.sort(OPTIMIZED_PACKAGES);
165 }
166
167
168 /**
169 * IMPLEMENTATION NOTE: This must be kept in sync with the Key
170 * in UIComponentBase$AttributesMap and HtmlComponentGenerator.
171 *
172 * Hopefully JSF 2.0 will remove the need for this.
173 */
174 private static final String ATTRIBUTES_THAT_ARE_SET_KEY =
175 UIComponentBase.class.getName() + ".attributesThatAreSet";
176
177
178 protected static final Logger LOGGER = FacesLogger.RENDERKIT.getLogger();
179
180
181 // ------------------------------------------------------------ Constructors
182
183
184 private RenderKitUtils() {
185 }
186
187
188 // ---------------------------------------------------------- Public Methods
189
190
191 /**
192 * <p>Return the {@link RenderKit} for the current request.</p>
193 * @param context the {@link FacesContext} of the current request
194 * @return the {@link RenderKit} for the current request.
195 */
196 public static RenderKit getCurrentRenderKit(FacesContext context) {
197
198 RenderKitFactory renderKitFactory = (RenderKitFactory)
199 FactoryFinder.getFactory(FactoryFinder.RENDER_KIT_FACTORY);
200 return renderKitFactory.getRenderKit(context,
201 context
202 .getViewRoot().getRenderKitId());
203
204 }
205
206
207 /**
208 * <p>Obtain and return the {@link ResponseStateManager} for
209 * the specified #renderKitId.</p>
210 *
211 * @param context the {@link FacesContext} of the current request
212 * @param renderKitId {@link RenderKit} ID
213 * @return the {@link ResponseStateManager} for the specified
214 * #renderKitId
215 * @throws FacesException if an exception occurs while trying
216 * to obtain the <code>ResponseStateManager</code>
217 */
218 public static ResponseStateManager getResponseStateManager(
219 FacesContext context, String renderKitId)
220 throws FacesException {
221
222 assert (null != renderKitId);
223 assert (null != context);
224
225 RenderKit renderKit = context.getRenderKit();
226 if (renderKit == null) {
227 // check request scope for a RenderKitFactory implementation
228 RenderKitFactory factory = (RenderKitFactory)
229 RequestStateManager.get(context, RequestStateManager.RENDER_KIT_IMPL_REQ);
230 if (factory != null) {
231 renderKit = factory.getRenderKit(context, renderKitId);
232 } else {
233 factory = (RenderKitFactory)
234 FactoryFinder
235 .getFactory(FactoryFinder.RENDER_KIT_FACTORY);
236 if (factory == null) {
237 throw new IllegalStateException();
238 } else {
239 RequestStateManager.set(context,
240 RequestStateManager.RENDER_KIT_IMPL_REQ,
241 factory);
242 }
243 renderKit = factory.getRenderKit(context, renderKitId);
244 }
245 }
246 return renderKit.getResponseStateManager();
247
248 }
249
250 /**
251 * <p>Return a List of {@link javax.faces.model.SelectItem}
252 * instances representing the available options for this component,
253 * assembled from the set of {@link javax.faces.component.UISelectItem}
254 * and/or {@link javax.faces.component.UISelectItems} components that are
255 * direct children of this component. If there are no such children, an
256 * empty <code>Iterator</code> is returned.</p>
257 *
258 * @param context The {@link javax.faces.context.FacesContext} for the current request.
259 * If null, the UISelectItems behavior will not work.
260 * @param component the component
261 * @throws IllegalArgumentException if <code>context</code>
262 * is <code>null</code>
263 * @return a List of the select items for the specified component
264 */
265 public static List<SelectItem> getSelectItems(FacesContext context,
266 UIComponent component) {
267
268 if (context == null) {
269 throw new IllegalArgumentException(
270 MessageUtils.getExceptionMessageString(
271 MessageUtils.NULL_PARAMETERS_ERROR_MESSAGE_ID, "context"));
272 }
273
274 ArrayList<SelectItem> list = new ArrayList<SelectItem>();
275 for (UIComponent kid : component.getChildren()) {
276 if (kid instanceof UISelectItem) {
277 UISelectItem item = (UISelectItem) kid;
278 Object value = item.getValue();
279
280 if (value == null) {
281 list.add(new SelectItem(item.getItemValue(),
282 item.getItemLabel(),
283 item.getItemDescription(),
284 item.isItemDisabled(),
285 item.isItemEscaped()));
286 } else if (value instanceof SelectItem) {
287 list.add((SelectItem) value);
288 } else {
289 throw new IllegalArgumentException(
290 MessageUtils.getExceptionMessageString(
291 MessageUtils.VALUE_NOT_SELECT_ITEM_ID,
292 component.getId(),
293 value.getClass().getName()));
294 }
295 } else if (kid instanceof UISelectItems) {
296 Object value = ((UISelectItems) kid).getValue();
297 if (value instanceof SelectItem) {
298 list.add((SelectItem) value);
299 } else if (value instanceof SelectItem[]) {
300 SelectItem[] items = (SelectItem[]) value;
301 // we manually copy the elements so that the list is
302 // modifiable. Arrays.asList() returns a non-mutable
303 // list.
304 //noinspection ManualArrayToCollectionCopy
305 for (SelectItem item : items) {
306 list.add(item);
307 }
308 } else if (value instanceof Collection) {
309 for (Object element : ((Collection) value)) {
310 if (SelectItem.class.isInstance(element)) {
311 list.add((SelectItem) element);
312 } else {
313 throw new IllegalArgumentException(
314 MessageUtils.getExceptionMessageString(
315 MessageUtils.VALUE_NOT_SELECT_ITEM_ID,
316 component.getId(),
317 value.getClass().getName()));
318 }
319 }
320 } else if (value instanceof Map) {
321 Map optionMap = (Map) value;
322 for (Object o : optionMap.entrySet()) {
323 Entry entry = (Entry) o;
324 Object key = entry.getKey();
325 Object val = entry.getValue();
326 if (key == null || val == null) {
327 continue;
328 }
329 list.add(new SelectItem(val,
330 key.toString()));
331 }
332 } else {
333 throw new IllegalArgumentException(
334 MessageUtils.getExceptionMessageString(
335 MessageUtils.CHILD_NOT_OF_EXPECTED_TYPE_ID,
336 "UISelectItem/UISelectItems",
337 component.getFamily(),
338 component.getId(),
339 value != null ? value.getClass().getName() : "null"));
340 }
341 }
342 }
343 return (list);
344
345 }
346
347
348 /**
349 * <p>Render any "passthru" attributes, where we simply just output the
350 * raw name and value of the attribute. This method is aware of the
351 * set of HTML4 attributes that fall into this bucket. Examples are
352 * all the javascript attributes, alt, rows, cols, etc. </p>
353 *
354 * @param writer writer the {@link javax.faces.context.ResponseWriter} to be used when writing
355 * the attributes
356 * @param component the component
357 * @param attributes an array off attributes to be processed
358 * @throws IOException if an error occurs writing the attributes
359 */
360 public static void renderPassThruAttributes(ResponseWriter writer,
361 UIComponent component,
362 String[] attributes)
363 throws IOException {
364
365 assert (null != writer);
366 assert (null != component);
367
368 Map<String, Object> attrMap = component.getAttributes();
369
370 // PENDING - think anyone would run the RI using another implementation
371 // of the jsf-api? If they did, then this would fall apart. That
372 // scenario seems extremely unlikely.
373 if (canBeOptimized(component)) {
374 //noinspection unchecked
375 List<String> setAttributes = (List<String>)
376 component.getAttributes().get(ATTRIBUTES_THAT_ARE_SET_KEY);
377 if (setAttributes != null) {
378 renderPassThruAttributesOptimized(writer,
379 component,
380 attributes,
381 setAttributes);
382 }
383 } else {
384 // this block should only be hit by custom components leveraging
385 // the RI's rendering code. We make no assumptions and loop through
386 // all known attributes.
387 boolean isXhtml =
388 RIConstants.XHTML_CONTENT_TYPE.equals(writer.getContentType());
389 for (String attrName : attributes) {
390
391 Object value =
392 attrMap.get(attrName);
393 if (value != null && shouldRenderAttribute(value)) {
394 writer.writeAttribute(prefixAttribute(attrName, isXhtml),
395 value,
396 attrName);
397 }
398 }
399
400 }
401 }
402
403
404 public static String prefixAttribute(final String attrName,
405 final ResponseWriter writer) {
406
407 return (prefixAttribute(attrName,
408 RIConstants.XHTML_CONTENT_TYPE.equals(writer.getContentType())));
409
410 }
411
412
413 public static String prefixAttribute(final String attrName,
414 boolean isXhtml) {
415 if (isXhtml) {
416 if (Arrays.binarySearch(XHTML_PREFIX_ATTRIBUTES, attrName) > -1) {
417 return XHTML_ATTR_PREFIX + attrName;
418 } else {
419 return attrName;
420 }
421 } else {
422 return attrName;
423 }
424
425 }
426
427
428 /**
429 * <p>Renders the attributes from {@link #BOOLEAN_ATTRIBUTES}
430 * using <code>XHMTL</code> semantics (i.e., disabled="disabled").</p>
431 *
432 * @param writer writer the {@link ResponseWriter} to be used when writing
433 * the attributes
434 * @param component the component
435 * @throws IOException if an error occurs writing the attributes
436 */
437 public static void renderXHTMLStyleBooleanAttributes(ResponseWriter writer,
438 UIComponent component)
439 throws IOException {
440
441 assert (writer != null);
442 assert (component != null);
443
444 Map attrMap = component.getAttributes();
445 for (String attrName : BOOLEAN_ATTRIBUTES) {
446 Object val = attrMap.get(attrName);
447 if (val == null) {
448 continue;
449 }
450
451 if (Boolean.valueOf(val.toString())) {
452 writer.writeAttribute(attrName,
453 true,
454 attrName);
455 }
456 }
457
458 }
459
460 /**
461 * <p>Given an accept String from the client, and a <code>String</code>
462 * of server supported content types, determine the best qualified
463 * content type for the client. If no match is found, or either of the
464 * arguments are <code>null</code>, <code>null</code> is returned.</p>
465 *
466 * @param accept The client accept String
467 * @param serverSupportedTypes The types that the server supports
468 * @param preferredType The preferred content type if another type is found
469 * with the same highest quality factor.
470 * @return The content type <code>String</code>
471 */
472 public static String determineContentType(String accept, String serverSupportedTypes, String preferredType) {
473 String contentType = null;
474
475 if (null == accept || null == serverSupportedTypes) {
476 return contentType;
477 }
478
479 String[][] clientContentTypes = buildTypeArrayFromString(accept);
480 String[][] serverContentTypes = buildTypeArrayFromString(serverSupportedTypes);
481 String[][] preferredContentType = buildTypeArrayFromString(preferredType);
482 String[][] matchedInfo = findMatch(clientContentTypes, serverContentTypes, preferredContentType);
483
484 // if best match exits and best match is not some wildcard,
485 // return best match
486 if ((matchedInfo[0][1] != null) && !(matchedInfo[0][2].equals("*"))) {
487 contentType = matchedInfo[0][1] + CONTENT_TYPE_SUBTYPE_DELIMITER + matchedInfo[0][2];
488 }
489
490 return contentType;
491 }
492
493
494 /**
495 * @param contentType the content type in question
496 * @return <code>true</code> if the content type is a known XML-based
497 * content type, otherwise, <code>false</code>
498 */
499 public static boolean isXml(String contentType) {
500 return (RIConstants.XHTML_CONTENT_TYPE.equals(contentType)
501 || RIConstants.APPLICATION_XML_CONTENT_TYPE.equals(contentType)
502 || RIConstants.TEXT_XML_CONTENT_TYPE.equals(contentType));
503 }
504
505
506 // --------------------------------------------------------- Private Methods
507
508
509 /**
510 * @param component the UIComponent in question
511 * @return <code>true</code> if the component is within the
512 * <code>javax.faces.component</code> or <code>javax.faces.component.html</code>
513 * packages, otherwise return <code>false</code>
514 */
515 private static boolean canBeOptimized(UIComponent component) {
516
517 Package p = component.getClass().getPackage();
518 return (p != null && (Arrays
519 .binarySearch(OPTIMIZED_PACKAGES, p.getName()) >= 0));
520
521 }
522
523
524 /**
525 * <p>For each attribute in <code>setAttributes</code>, perform a binary
526 * search against the array of <code>knownAttributes</code> If a match is found
527 * and the value is not <code>null</code>, render the attribute.
528 * @param writer the current writer
529 * @param component the component whose attributes we're rendering
530 * @param knownAttributes an array of pass-through attributes supported by
531 * this component
532 * @param setAttributes a <code>List</code> of attributes that have been set
533 * on the provided component
534 * @throws IOException if an error occurs during the write
535 */
536 private static void renderPassThruAttributesOptimized(ResponseWriter writer,
537 UIComponent component,
538 String[] knownAttributes,
539 List<String> setAttributes)
540 throws IOException {
541
542 String[] attributes = setAttributes.toArray(new String[setAttributes.size()]);
543 Arrays.sort(attributes);
544 boolean isXhtml =
545 RIConstants.XHTML_CONTENT_TYPE.equals(writer.getContentType());
546 Map<String, Object> attrMap = component.getAttributes();
547 for (String name : attributes) {
548 if (Arrays.binarySearch(knownAttributes, name) >= 0) {
549 Object value =
550 attrMap.get(name);
551 if (value != null && shouldRenderAttribute(value)) {
552 writer.writeAttribute(prefixAttribute(name, isXhtml),
553 value,
554 name);
555 }
556 }
557 }
558
559 }
560
561
562 /**
563 * <p>Determines if an attribute should be rendered based on the
564 * specified #attributeVal.</p>
565 *
566 * @param attributeVal the attribute value
567 * @return <code>true</code> if and only if #attributeVal is
568 * an instance of a wrapper for a primitive type and its value is
569 * equal to the default value for that type as given in the specification.
570 */
571 private static boolean shouldRenderAttribute(Object attributeVal) {
572
573 if (attributeVal instanceof String) {
574 return true;
575 } else if (attributeVal instanceof Boolean &&
576 Boolean.FALSE.equals(attributeVal)) {
577 return false;
578 } else if (attributeVal instanceof Integer &&
579 (Integer) attributeVal == Integer.MIN_VALUE) {
580 return false;
581 } else if (attributeVal instanceof Double &&
582 (Double) attributeVal == Double.MIN_VALUE) {
583 return false;
584 } else if (attributeVal instanceof Character &&
585 (Character) attributeVal
586 == Character
587 .MIN_VALUE) {
588 return false;
589 } else if (attributeVal instanceof Float &&
590 (Float) attributeVal == Float.MIN_VALUE) {
591 return false;
592 } else if (attributeVal instanceof Short &&
593 (Short) attributeVal == Short.MIN_VALUE) {
594 return false;
595 } else if (attributeVal instanceof Byte &&
596 (Byte) attributeVal == Byte.MIN_VALUE) {
597 return false;
598 } else if (attributeVal instanceof Long &&
599 (Long) attributeVal == Long.MIN_VALUE) {
600 return false;
601 }
602 return true;
603
604 }
605
606 /**
607 * <p>This method builds a two element array structure as follows:
608 * Example:
609 * Given the following accept string:
610 * text/html; level=1, text/plain; q=0.5
611 * [0][0] 1 (quality is 1 if none specified)
612 * [0][1] "text" (type)
613 * [0][2] "html; level=1" (subtype)
614 * [0][3] 1 (level, if specified; null if not)
615 *
616 * [1][0] .5
617 * [1][1] "text"
618 * [1][2] "plain"
619 * [1][3] (level, if specified; null if not)
620 *
621 * The array is used for comparison purposes in the findMatch method.</p>
622 *
623 * @param accept An accept <code>String</code>
624 * @return an two dimensional array containing content-type/quality info
625 */
626 private static String[][] buildTypeArrayFromString(String accept) {
627 String[][] arrayAccept = new String[MAX_CONTENT_TYPES][MAX_CONTENT_TYPE_PARTS];
628 // return if empty
629 if ((accept == null) || (accept.length() == 0))
630 return arrayAccept;
631 // some helper variables
632 StringBuilder typeSubType;
633 String type;
634 String subtype;
635 String level = null;
636 String quality = null;
637
638 // Parse "types"
639 String[] types = Util.split(accept, CONTENT_TYPE_DELIMITER);
640 int index = -1;
641 for (int i=0; i<types.length; i++) {
642 String token = types[i].trim();
643 index += 1;
644 // Check to see if our accept string contains the delimiter that is used
645 // to add uniqueness to a type/subtype, and/or delimits a qualifier value:
646 // Example: text/html;level=1,text/html;level=2; q=.5
647 if (token.contains(";")) {
648 String[] typeParts = Util.split(token, ";");
649 typeSubType = new StringBuilder(typeParts[0].trim());
650 for (int j=1; j<typeParts.length; j++) {
651 quality = "not set";
652 token = typeParts[j].trim();
653 // if "level" is present, make sure it gets included in the "type/subtype"
654 if (token.contains("level")) {
655 typeSubType.append(';').append(token);
656 String[] levelParts = Util.split(token, "=");
657 level = levelParts[0].trim();
658 if (level.equalsIgnoreCase("level")) {
659 level = levelParts[1].trim();
660 }
661 } else {
662 quality = token;
663 String[] qualityParts = Util.split(quality, "=");
664 quality = qualityParts[0].trim();
665 if (quality.equalsIgnoreCase("q")) {
666 quality = qualityParts[1].trim();
667 break;
668 } else {
669 quality = "not set"; // to identifiy that no quality was supplied
670 }
671 }
672 }
673 } else {
674 typeSubType = new StringBuilder(token);
675 quality = "not set"; // to identifiy that no quality was supplied
676 }
677 // now split type and subtype
678 if (typeSubType.indexOf(CONTENT_TYPE_SUBTYPE_DELIMITER) >= 0) {
679 String[] typeSubTypeParts = Util.split(typeSubType.toString(), CONTENT_TYPE_SUBTYPE_DELIMITER);
680 type = typeSubTypeParts[0].trim();
681 subtype = typeSubTypeParts[1].trim();
682 } else {
683 type = typeSubType.toString();
684 subtype = "";
685 }
686 // check quality and assign values
687 if ("not set".equals(quality)) {
688 if (type.equals("*") && subtype.equals("*")) {
689 quality = "0.01";
690 } else if (!type.equals("*") && subtype.equals("*")) {
691 quality = "0.02";
692 } else if (type.equals("*") && subtype.length() == 0) {
693 quality = "0.01";
694 } else {
695 quality = "1";
696 }
697 }
698 arrayAccept[index][0] = quality;
699 arrayAccept[index][1] = type;
700 arrayAccept[index][2] = subtype;
701 arrayAccept[index][3] = level;
702 }
703 return (arrayAccept);
704 }
705
706 /**
707 * <p>For each server supported type, compare client (browser) specified types.
708 * If a match is found, keep track of the highest quality factor.
709 * The end result is that for all matches, only the one with the highest
710 * quality will be returned.</p>
711 *
712 * @param clientContentTypes An <code>array</code> of accept <code>String</code>
713 * information for the client built from @{link #buildTypeArrayFromString}.
714 * @param serverSupportedContentTypes An <code>array</code> of accept <code>String</code>
715 * information for the server supported types built from @{link #buildTypeArrayFromString}.
716 * @param preferredContentType An <code>array</code> of preferred content type information.
717 * @return An <code>array</code> containing the parts of the preferred content type for the
718 * client. The information is stored as outlined in @{link #buildTypeArrayFromString}.
719 */
720 private static String[][] findMatch(String[][] clientContentTypes,
721 String[][] serverSupportedContentTypes,
722 String[][] preferredContentType) {
723
724 // result array
725 String[][] results = new String[MAX_CONTENT_TYPES][MAX_CONTENT_TYPE_PARTS];
726 int resultidx = -1;
727 // the highest quality
728 double highestQFactor = 0;
729 // the record with the highest quality
730 int idx = 0;
731 for (int sidx = 0; sidx < MAX_CONTENT_TYPES; sidx++) {
732 // get server type
733 String serverType = serverSupportedContentTypes[sidx][1];
734 if (serverType != null) {
735 for (int cidx = 0; cidx < MAX_CONTENT_TYPES; cidx++) {
736 // get browser type
737 String browserType = clientContentTypes[cidx][1];
738 if (browserType != null) {
739 // compare them and check for wildcard
740 if ((browserType.equalsIgnoreCase(serverType)) || (browserType.equals("*"))) {
741 // types are equal or browser type is wildcard - compare subtypes
742 if ((clientContentTypes[cidx][2].equalsIgnoreCase(
743 serverSupportedContentTypes[sidx][2])) ||
744 (clientContentTypes[cidx][2].equals("*"))) {
745 // subtypes are equal or browser subtype is wildcard
746 // found match: multiplicate qualities and add to result array
747 // if there was a level associated, this gets higher precedence, so
748 // factor in the level in the calculation.
749 double cLevel = 0.0;
750 double sLevel = 0.0;
751 if (clientContentTypes[cidx][3] != null) {
752 cLevel = (Double.parseDouble(clientContentTypes[cidx][3]))*.10;
753 }
754 if (serverSupportedContentTypes[sidx][3] != null) {
755 sLevel = (Double.parseDouble(serverSupportedContentTypes[sidx][3]))*.10;
756 }
757 double cQfactor = Double.parseDouble(clientContentTypes[cidx][0]) + cLevel;
758 double sQfactor = Double.parseDouble(serverSupportedContentTypes[sidx][0]) + sLevel;
759 double resultQuality = cQfactor * sQfactor;
760 resultidx += 1;
761 results[resultidx][0] = String.valueOf(resultQuality);
762 if (clientContentTypes[cidx][2].equals("*")) {
763 // browser subtype is wildcard
764 // return type and subtype (wildcard)
765 results[resultidx][1] = clientContentTypes[cidx][1];
766 results[resultidx][2] = clientContentTypes[cidx][2];
767 } else {
768 // return server type and subtype
769 results[resultidx][1] = serverSupportedContentTypes[sidx][1];
770 results[resultidx][2] = serverSupportedContentTypes[sidx][2];
771 results[resultidx][3] = serverSupportedContentTypes[sidx][3];
772 }
773 // check if this was the highest factor
774 if (resultQuality > highestQFactor) {
775 idx = resultidx;
776 highestQFactor = resultQuality;
777 }
778 }
779 }
780 }
781 }
782 }
783 }
784
785 // First, determine if we have a type that has the highest quality factor that
786 // also matches the preferred type (if there is one):
787 String[][] match = new String[1][3];
788 if (preferredContentType[0][0] != null) {
789 BigDecimal highestQual = BigDecimal.valueOf(highestQFactor);
790 for (int i=0; i<=resultidx; i++) {
791 if ((BigDecimal.valueOf(Double.parseDouble(results[i][0])).compareTo(highestQual) == 0) &&
792 (results[i][1]).equals(preferredContentType[0][1]) &&
793 (results[i][2]).equals(preferredContentType[0][2])) {
794 match[0][0] = results[i][0];
795 match[0][1] = results[i][1];
796 match[0][2] = results[i][2];
797 return match;
798 }
799 }
800 }
801
802 match[0][0] = results[idx][0];
803 match[0][1] = results[idx][1];
804 match[0][2] = results[idx][2];
805 return match;
806 }
807
808 /**
809 * <p>Replaces all occurrences of <code>-</code> with <code>$_</code>.</p>
810 *
811 * @param origIdentifier the original identifer that needs to be
812 * 'ECMA-ized'
813 * @return an ECMA valid identifer
814 */
815 public static String createValidECMAIdentifier(String origIdentifier) {
816 return origIdentifier.replace("-", "$_");
817 }
818
819
820 /**
821 * <p>Renders the Javascript necessary to add and remove request
822 * parameters to the current form.</p>
823 * @param writer the <code>ResponseWriter</code>
824 * @param context the <code>FacesContext</code> for the current request
825 * @throws java.io.IOException if an error occurs writing to the response
826 */
827 public static void renderFormInitScript(ResponseWriter writer,
828 FacesContext context)
829 throws IOException {
830 WebConfiguration webConfig =
831 WebConfiguration.getInstance(context.getExternalContext());
832
833 if (webConfig.isOptionEnabled(BooleanWebContextInitParameter.ExternalizeJavaScript)) {
834 // PENDING
835 // We need to look into how to make this work in a portlet environment.
836 // For the time being, this feature will need to be disabled when running
837 // in a portlet.
838 String mapping = Util.getFacesMapping(context);
839 String uri;
840 if ((mapping != null) && (Util.isPrefixMapped(mapping))) {
841 uri = mapping + '/' + RIConstants.SUN_JSF_JS_URI;
842 } else {
843 uri = '/' + RIConstants.SUN_JSF_JS_URI + mapping;
844 }
845 writer.write('\n');
846 writer.startElement("script", null);
847 writer.writeAttribute("type", "text/javascript", null);
848 writer.writeAttribute("src",
849 context.getExternalContext()
850 .getRequestContextPath() + uri,
851 null);
852 writer.endElement("script");
853 writer.write("\n");
854 } else {
855 writer.write('\n');
856 writer.startElement("script", null);
857 writer.writeAttribute("type", "text/javascript", null);
858 writer.writeAttribute("language", "Javascript", null);
859 writeSunJS(context, writer);
860 writer.endElement("script");
861 writer.write("\n");
862 }
863 }
864
865 /**
866 * <p>Returns a string that can be inserted into the <code>onclick</code>
867 * handler of a command. This string will add all request parameters
868 * as well as the client ID of the activated command to the form as
869 * hidden input parameters, update the target of the link if necessary,
870 * and handle the form submission. The content of {@link #SUN_JSF_JS}
871 * must be rendered prior to using this method.</p>
872 * @param formClientId the client ID of the form
873 * @param commandClientId the client ID of the command
874 * @param target the link target
875 * @param params the nested parameters, if any @return a String suitable for the <code>onclick</code> handler
876 * of a command
877 * @return the default <code>onclick</code> JavaScript for the default
878 * command link component
879 */
880 public static String getCommandLinkOnClickScript(String formClientId,
881 String commandClientId,
882 String target,
883 Param[] params) {
884
885 StringBuilder sb = new StringBuilder(256);
886 sb.append("if(typeof jsfcljs == 'function'){jsfcljs(document.getElementById('");
887 sb.append(formClientId);
888 sb.append("'),{'");
889 sb.append(commandClientId).append("':'").append(commandClientId);
890 for (Param param : params) {
891 String pn = param.name;
892 if (pn != null && pn.length() != 0) {
893 String pv = param.value;
894 sb.append("','");
895 sb.append(pn.replace("'", "\\\'"));
896 sb.append("':'");
897 if (pv != null && pv.length() != 0) {
898 sb.append(pv.replace("'", "\\\'"));
899 }
900 }
901 }
902 sb.append("'},'");
903 sb.append(target);
904 sb.append("');}return false");
905
906 return sb.toString();
907
908 }
909
910
911 /**
912 * <p>This is a utility method for compressing multi-lined javascript.
913 * In the case of {@link #SUN_JSF_JS} it offers about a 47% decrease
914 * in length.</p>
915 *
916 * <p>For our purposes, compression is just trimming each line and
917 * then writing it out. It's pretty simplistic, but it works.</p>
918 *
919 * @param JSString the string to compress
920 * @return the compressed string
921 */
922 public static char[] compressJS(String JSString) {
923
924 BufferedReader reader = new BufferedReader(new StringReader(JSString));
925 StringWriter writer = new StringWriter(1024);
926 try {
927 for (String line = reader.readLine();
928 line != null;
929 line = reader.readLine()) {
930
931 line = line.trim();
932 writer.write(line);
933 }
934 return writer.toString().toCharArray();
935 } catch (IOException ioe) {
936 // won't happen
937 }
938 return null;
939
940 }
941
942
943 /**
944 * <p>Return the implementation JavaScript. If compression
945 * is enabled, the result will be compressed.</p>
946 *
947 * @param context - the <code>FacesContext</code> for the current request
948 * @param writer - the <code>Writer</code> to write the JS to
949 * @throws IOException if the JavaScript cannot be written
950 *
951 */
952 public static void writeSunJS(FacesContext context, Writer writer)
953 throws IOException {
954 writer.write((char[]) context.getExternalContext().getApplicationMap()
955 .get(SUN_JSF_JS));
956 }
957
958
959 // --------------------------------------------------------- Private Methods
960
961
962 /**
963 * <p>Loads the contents of the sunjsf.js file into memory removing any
964 * comments/empty lines it encoutners, and, if enabled, compressing the
965 * result.</p> This method should only be called when the application is
966 * being initialized.
967 * @param extContext the ExternalContext for this application
968 */
969 public synchronized static void loadSunJsfJs(ExternalContext extContext) {
970 Map<String, Object> appMap =
971 extContext.getApplicationMap();
972 char[] sunJsfJs;
973
974 BufferedReader reader = null;
975 try {
976 URL url = Util.getCurrentLoader(appMap)
977 .getResource("com/sun/faces/sunjsf.js");
978 if (url == null) {
979 LOGGER.severe(
980 "jsf.renderkit.util.cannot_load_js");
981 return;
982 }
983 URLConnection conn = url.openConnection();
984 conn.setUseCaches(false);
985 InputStream input = conn.getInputStream();
986 reader = new BufferedReader(
987 new InputStreamReader(input));
988 StringBuilder builder = new StringBuilder(128);
989 for (String line = reader.readLine();
990 line != null;
991 line = reader.readLine()) {
992
993 String temp = line.trim();
994 if (temp.length() == 0
995 || temp.startsWith("/*")
996 || temp.startsWith("*")
997 || temp.startsWith("*/")
998 || temp.startsWith("//")) {
999 continue;
1000 }
1001 builder.append(line).append('\n');
1002 }
1003 builder.deleteCharAt(builder.length() - 1);
1004 if (WebConfiguration
1005 .getInstance(extContext)
1006 .isOptionEnabled(BooleanWebContextInitParameter.CompressJavaScript)) {
1007 sunJsfJs = compressJS(builder.toString());
1008 } else {
1009 sunJsfJs = builder.toString().toCharArray();
1010 }
1011 appMap.put(SUN_JSF_JS, sunJsfJs);
1012 } catch (IOException ioe) {
1013 LOGGER.log(Level.SEVERE,
1014 "jsf.renderkit.util.cannot_load_js",
1015 ioe);
1016 } finally {
1017 if (reader != null) {
1018 try {
1019 reader.close();
1020 } catch (IOException ioe) {
1021 // ignore
1022 }
1023 }
1024 }
1025 }
1026
1027 } // END RenderKitUtils