/** * This utility class is to provide both encryption and decryption <code>Ciphers</code> to <code> * ResponseStateManager</code> implementations wishing to provide encryption support. * * <p>The algorithm used to encrypt byte array is AES with CBC. * * <p>Original author Inderjeet Singh, J2EE Blue Prints Team. Modified to suit JSF needs. */ public final class ByteArrayGuard { // Log instance for this class private static final Logger LOGGER = FacesLogger.RENDERKIT.getLogger(); private static final int MAC_LENGTH = 32; private static final int KEY_LENGTH = 128; private static final int IV_LENGTH = 16; private static final String KEY_ALGORITHM = "AES"; private static final String CIPHER_CODE = "AES/CBC/PKCS5Padding"; private static final String MAC_CODE = "HmacSHA256"; private SecretKey sk; // ------------------------------------------------------------ Constructors public ByteArrayGuard() { try { setupKeyAndMac(); } catch (Exception e) { if (LOGGER.isLoggable(Level.SEVERE)) { LOGGER.log( Level.SEVERE, "Unexpected exception initializing encryption." + " No encryption will be performed.", e); } System.err.println("ERROR: Initializing Ciphers"); } } // ---------------------------------------------------------- Public Methods /** * This method: Encrypts bytes using a cipher. Generates MAC for intialization vector of the * cipher Generates MAC for encrypted data Returns a byte array consisting of the following * concatenated together: |MAC for cnrypted Data | MAC for Init Vector | Encrypted Data | * * @param bytes The byte array to be encrypted. * @return the encrypted byte array. */ public byte[] encrypt(byte[] bytes) { byte[] securedata = null; try { // Generate IV SecureRandom rand = new SecureRandom(); byte[] iv = new byte[16]; rand.nextBytes(iv); IvParameterSpec ivspec = new IvParameterSpec(iv); Cipher encryptCipher = Cipher.getInstance(CIPHER_CODE); encryptCipher.init(Cipher.ENCRYPT_MODE, sk, ivspec); Mac encryptMac = Mac.getInstance(MAC_CODE); encryptMac.init(sk); encryptMac.update(iv); // encrypt the plaintext byte[] encdata = encryptCipher.doFinal(bytes); byte[] macBytes = encryptMac.doFinal(encdata); byte[] tmp = concatBytes(macBytes, iv); securedata = concatBytes(tmp, encdata); } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | InvalidAlgorithmParameterException | IllegalStateException | IllegalBlockSizeException | BadPaddingException e) { if (LOGGER.isLoggable(Level.SEVERE)) { LOGGER.log( Level.SEVERE, "Unexpected exception initializing encryption." + " No encryption will be performed.", e); } return null; } return securedata; } /** * This method decrypts the provided byte array. The decryption is only performed if the * regenerated MAC is the same as the MAC for the received value. * * @param bytes Encrypted byte array to be decrypted. * @return Decrypted byte array. */ public byte[] decrypt(byte[] bytes) { try { // Extract MAC byte[] macBytes = new byte[MAC_LENGTH]; System.arraycopy(bytes, 0, macBytes, 0, macBytes.length); // Extract IV byte[] iv = new byte[IV_LENGTH]; System.arraycopy(bytes, macBytes.length, iv, 0, iv.length); // Extract encrypted data byte[] encdata = new byte[bytes.length - macBytes.length - iv.length]; System.arraycopy(bytes, macBytes.length + iv.length, encdata, 0, encdata.length); IvParameterSpec ivspec = new IvParameterSpec(iv); Cipher decryptCipher = Cipher.getInstance(CIPHER_CODE); decryptCipher.init(Cipher.DECRYPT_MODE, sk, ivspec); // verify MAC by regenerating it and comparing it with the received value Mac decryptMac = Mac.getInstance(MAC_CODE); decryptMac.init(sk); decryptMac.update(iv); decryptMac.update(encdata); byte[] macBytesCalculated = decryptMac.doFinal(); if (areArrayEqualsConstantTime(macBytes, macBytesCalculated)) { // continue only if the MAC was valid // System.out.println("Valid MAC found!"); byte[] plaindata = decryptCipher.doFinal(encdata); return plaindata; } else { System.err.println("ERROR: MAC did not verify!"); return null; } } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | InvalidAlgorithmParameterException | IllegalStateException | IllegalBlockSizeException | BadPaddingException e) { System.err.println("ERROR: Decrypting:" + e.getCause()); return null; // Signal to JSF runtime } } private boolean areArrayEqualsConstantTime(byte[] array1, byte[] array2) { boolean result = true; for (int i = 0; i < array1.length; i++) { if (array1[i] != array2[i]) { result = false; } } return result; } // --------------------------------------------------------- Private Methods /** Generates secret key. Initializes MAC(s). */ private void setupKeyAndMac() { /* * Lets see if an encoded key was given to the application, if so use * it and skip the code to generate it. */ try { InitialContext context = new InitialContext(); String encodedKeyArray = (String) context.lookup("java:comp/env/jsf/ClientSideSecretKey"); byte[] keyArray = DatatypeConverter.parseBase64Binary(encodedKeyArray); sk = new SecretKeySpec(keyArray, KEY_ALGORITHM); } catch (NamingException exception) { if (LOGGER.isLoggable(Level.FINEST)) { LOGGER.log(Level.FINEST, "Unable to find the encoded key.", exception); } } if (sk == null) { try { KeyGenerator kg = KeyGenerator.getInstance(KEY_ALGORITHM); kg.init(KEY_LENGTH); // 256 if you're using the Unlimited Policy Files sk = kg.generateKey(); // System.out.print("SecretKey: " + // DatatypeConverter.printBase64Binary(sk.getEncoded())); } catch (Exception e) { throw new FacesException(e); } } } /** * This method concatenates two byte arrays * * @return a byte array of array1||array2 * @param array1 first byte array to be concatenated * @param array2 second byte array to be concatenated */ private static byte[] concatBytes(byte[] array1, byte[] array2) { byte[] cBytes = new byte[array1.length + array2.length]; try { System.arraycopy(array1, 0, cBytes, 0, array1.length); System.arraycopy(array2, 0, cBytes, array1.length, array2.length); } catch (Exception e) { throw new FacesException(e); } return cBytes; } }
/** <B>HtmlBasicRenderer</B> is a base class for implementing renderers for HtmlBasicRenderKit. */ public abstract class HtmlBasicRenderer extends Renderer { // Log instance for this class protected static final Logger logger = FacesLogger.RENDERKIT.getLogger(); protected static final Param[] EMPTY_PARAMS = new Param[0]; // ------------------------------------------------------------ Constructors public HtmlBasicRenderer() { super(); } // ---------------------------------------------------------- Public Methods @Override public String convertClientId(FacesContext context, String clientId) { return clientId; } @Override public void decode(FacesContext context, UIComponent component) { rendererParamsNotNull(context, component); if (!shouldDecode(component)) { return; } String clientId = decodeBehaviors(context, component); if (!(component instanceof UIInput)) { // decode needs to be invoked only for components that are // instances or subclasses of UIInput. if (logger.isLoggable(Level.FINE)) { logger.log( Level.FINE, "No decoding necessary since the component {0} is not an instance or a sub class of UIInput", component.getId()); } return; } if (clientId == null) { clientId = component.getClientId(context); } assert (clientId != null); Map<String, String> requestMap = context.getExternalContext().getRequestParameterMap(); // Don't overwrite the value unless you have to! String newValue = requestMap.get(clientId); if (newValue != null) { setSubmittedValue(component, newValue); if (logger.isLoggable(Level.FINE)) { logger.log(Level.FINE, "new value after decoding {0}", newValue); } } } @Override public void encodeEnd(FacesContext context, UIComponent component) throws IOException { rendererParamsNotNull(context, component); if (!shouldEncode(component)) { return; } ResponseWriter writer = context.getResponseWriter(); assert (writer != null); String currentValue = getCurrentValue(context, component); if (logger.isLoggable(Level.FINE)) { logger.log(Level.FINE, "Value to be rendered {0}", currentValue); } getEndTextToRender(context, component, currentValue); } @Override public boolean getRendersChildren() { return true; } // ------------------------------------------------------- Protected Methods // Decodes Behaviors if any match the behavior source/event. // As a convenience, returns component id, but only if it // was retrieved. This allows us to avoid duplicating // calls to getClientId(), which can be expensive for // deep component trees. protected final String decodeBehaviors(FacesContext context, UIComponent component) { if (!(component instanceof ClientBehaviorHolder)) { return null; } ClientBehaviorHolder holder = (ClientBehaviorHolder) component; Map<String, List<ClientBehavior>> behaviors = holder.getClientBehaviors(); if (behaviors.isEmpty()) { return null; } String behaviorEvent = BEHAVIOR_EVENT_PARAM.getValue(context); if (null != behaviorEvent) { List<ClientBehavior> behaviorsForEvent = behaviors.get(behaviorEvent); if (behaviorsForEvent != null && behaviorsForEvent.size() > 0) { String behaviorSource = BEHAVIOR_SOURCE_PARAM.getValue(context); String clientId = component.getClientId(); if (isBehaviorSource(context, behaviorSource, clientId)) { for (ClientBehavior behavior : behaviorsForEvent) { behavior.decode(context, component); } } return clientId; } } return null; } /** * @param ctx the <code>FacesContext</code> for the current request * @param behaviorSourceId the ID of the behavior source * @param componentClientId the client ID of the component being decoded * @return <code>true</code> if the behavior source is for the component being decoded, otherwise * <code>false</code> */ protected boolean isBehaviorSource( FacesContext ctx, String behaviorSourceId, String componentClientId) { return (behaviorSourceId != null && behaviorSourceId.equals(componentClientId)); } /** * <p>Conditionally augment an id-reference value.</p> * <p>If the <code>forValue</code> doesn't already include a generated * suffix, but the id of the <code>fromComponent</code> does include a * generated suffix, then append the suffix from the * <code>fromComponent</code> to the <code>forValue</code>. * Otherwise just return the <code>forValue</code> as is.</p> * * @param forValue - the basic id-reference value. * @param fromComponent - the component that holds the * code>forValue</code>. * * @return the (possibly augmented) <code>forValue<code>. */ protected String augmentIdReference(String forValue, UIComponent fromComponent) { int forSuffix = forValue.lastIndexOf(UIViewRoot.UNIQUE_ID_PREFIX); if (forSuffix <= 0) { // if the for-value doesn't already have a suffix present String id = fromComponent.getId(); if (id != null) { int idSuffix = id.lastIndexOf(UIViewRoot.UNIQUE_ID_PREFIX); if (idSuffix > 0) { // but the component's own id does have a suffix if (logger.isLoggable(Level.FINE)) { logger.fine( "Augmenting for attribute with " + id.substring(idSuffix) + " suffix from Id attribute"); } forValue += id.substring(idSuffix); } } } return forValue; } /** * Render nested child components by invoking the encode methods on those components, but only * when the <code>rendered</code> property is <code>true</code>. * * @param context FacesContext for the current request * @param component the component to recursively encode * @throws IOException if an error occurrs during the encode process */ protected void encodeRecursive(FacesContext context, UIComponent component) throws IOException { // suppress rendering if "rendered" property on the component is // false. if (!component.isRendered()) { return; } // Render this component and its children recursively component.encodeBegin(context); if (component.getRendersChildren()) { component.encodeChildren(context); } else { Iterator<UIComponent> kids = getChildren(component); while (kids.hasNext()) { UIComponent kid = kids.next(); encodeRecursive(context, kid); } } component.encodeEnd(context); } /** * @param component <code>UIComponent</code> for which to extract children * @return an Iterator over the children of the specified component, selecting only those that * have a <code>rendered</code> property of <code>true</code>. */ protected Iterator<UIComponent> getChildren(UIComponent component) { int childCount = component.getChildCount(); if (childCount > 0) { return component.getChildren().iterator(); } else { return Collections.<UIComponent>emptyList().iterator(); } } /** * @param context the FacesContext for the current request * @param component the UIComponent whose value we're interested in * @return the value to be rendered and formats it if required. Sets to empty string if value is * null. */ protected String getCurrentValue(FacesContext context, UIComponent component) { if (component instanceof UIInput) { Object submittedValue = ((UIInput) component).getSubmittedValue(); if (submittedValue != null) { // value may not be a String... return submittedValue.toString(); } } String currentValue = null; Object currentObj = getValue(component); if (currentObj != null) { currentValue = getFormattedValue(context, component, currentObj); } return currentValue; } /** * Renderers override this method to write appropriate HTML content into the buffer. * * @param context the FacesContext for the current request * @param component the UIComponent of interest * @param currentValue <code>component</code>'s current value * @throws IOException if an error occurs rendering the text */ protected void getEndTextToRender( FacesContext context, UIComponent component, String currentValue) throws IOException { // no-op unless overridden } /** * @param component Component from which to return a facet * @param name Name of the desired facet * @return the specified facet from the specified component, but <strong>only</strong> if its * <code>rendered</code> property is set to <code>true</code>. */ protected UIComponent getFacet(UIComponent component, String name) { UIComponent facet = null; if (component.getFacetCount() > 0) { facet = component.getFacet(name); if ((facet != null) && !facet.isRendered()) { facet = null; } } return (facet); } /** * Locates the component identified by <code>forComponent</code> * * @param context the FacesContext for the current request * @param forComponent - the component to search for * @param component - the starting point in which to begin the search * @return the component with the the <code>id</code that matches * <code> * forComponent</code> otheriwse null if no match is found. */ protected UIComponent getForComponent( FacesContext context, String forComponent, UIComponent component) { if (null == forComponent || forComponent.length() == 0) { return null; } UIComponent result = null; UIComponent currentParent = component; try { // Check the naming container of the current // component for component identified by // 'forComponent' while (currentParent != null) { // If the current component is a NamingContainer, // see if it contains what we're looking for. result = currentParent.findComponent(forComponent); if (result != null) { break; } // if not, start checking further up in the view currentParent = currentParent.getParent(); } // no hit from above, scan for a NamingContainer // that contains the component we're looking for from the root. if (result == null) { result = findUIComponentBelow(context.getViewRoot(), forComponent); } } catch (Exception e) { if (logger.isLoggable(Level.FINEST)) { logger.log(Level.FINEST, "Unable to find for component", e); } } // log a message if we were unable to find the specified // component (probably a misconfigured 'for' attribute if (result == null) { if (logger.isLoggable(Level.WARNING)) { logger.warning( MessageUtils.getExceptionMessageString( MessageUtils.COMPONENT_NOT_FOUND_IN_VIEW_WARNING_ID, forComponent)); } } return result; } /** * Overloads getFormattedValue to take a advantage of a previously obtained converter. * * @param context the FacesContext for the current request * @param component UIComponent of interest * @param currentValue the current value of <code>component</code> * @param converter the component's converter * @return the currentValue after any associated Converter has been applied * @throws ConverterException if the value cannot be converted */ protected String getFormattedValue( FacesContext context, UIComponent component, Object currentValue, Converter converter) throws ConverterException { // formatting is supported only for components that support // converting value attributes. if (!(component instanceof ValueHolder)) { if (currentValue != null) { return currentValue.toString(); } return null; } if (converter == null) { // If there is a converter attribute, use it to to ask application // instance for a converter with this identifer. converter = ((ValueHolder) component).getConverter(); } if (converter == null) { // if value is null and no converter attribute is specified, then // return a zero length String. if (currentValue == null) { return ""; } // Do not look for "by-type" converters for Strings if (currentValue instanceof String) { return (String) currentValue; } // if converter attribute set, try to acquire a converter // using its class type. Class converterType = currentValue.getClass(); converter = Util.getConverterForClass(converterType, context); // if there is no default converter available for this identifier, // assume the model type to be String. if (converter == null) { return currentValue.toString(); } } return converter.getAsString(context, component, currentValue); } /** * @param context the FacesContext for the current request * @param component UIComponent of interest * @param currentValue the current value of <code>component</code> * @return the currentValue after any associated Converter has been applied * @throws ConverterException if the value cannot be converted */ protected String getFormattedValue( FacesContext context, UIComponent component, Object currentValue) throws ConverterException { return getFormattedValue(context, component, currentValue, null); } protected Iterator getMessageIter( FacesContext context, String forComponent, UIComponent component) { Iterator messageIter; // Attempt to use the "for" attribute to locate // messages. Three possible scenarios here: // 1. valid "for" attribute - messages returned // for valid component identified by "for" expression. // 2. zero length "for" expression - global errors // not associated with any component returned // 3. no "for" expression - all messages returned. if (null != forComponent) { if (forComponent.length() == 0) { messageIter = context.getMessages(null); } else { UIComponent result = getForComponent(context, forComponent, component); if (result == null) { messageIter = Collections.EMPTY_LIST.iterator(); } else { messageIter = context.getMessages(result.getClientId(context)); } } } else { messageIter = context.getMessages(); } return messageIter; } /** * @param command the command which may have parameters * @return an array of parameters */ protected Param[] getParamList(UIComponent command) { if (command.getChildCount() > 0) { ArrayList<Param> parameterList = new ArrayList<>(); for (UIComponent kid : command.getChildren()) { if (kid instanceof UIParameter) { UIParameter uiParam = (UIParameter) kid; if (!uiParam.isDisable()) { Object value = uiParam.getValue(); Param param = new Param(uiParam.getName(), (value == null ? null : value.toString())); parameterList.add(param); } } } return parameterList.toArray(new Param[parameterList.size()]); } else { return EMPTY_PARAMS; } } /** * Collections parameters for use with Behavior script rendering. Similar to getParamList(), but * returns a collection of ClientBehaviorContext.Parameter instances. * * @param command the command which may have parameters * @return a collection of ClientBehaviorContext.Parameter instances. */ protected Collection<ClientBehaviorContext.Parameter> getBehaviorParameters(UIComponent command) { ArrayList<ClientBehaviorContext.Parameter> params = null; int childCount = command.getChildCount(); if (childCount > 0) { for (UIComponent kid : command.getChildren()) { if (kid instanceof UIParameter) { UIParameter uiParam = (UIParameter) kid; String name = uiParam.getName(); Object value = uiParam.getValue(); if ((name != null) && (name.length() > 0)) { if (params == null) { params = new ArrayList<>(childCount); } params.add(new ClientBehaviorContext.Parameter(name, value)); } } } } return (params == null) ? Collections.<ClientBehaviorContext.Parameter>emptyList() : params; } protected Object getValue(UIComponent component) { // Make sure this method isn't being called except // from subclasses that override getValue()! throw new UnsupportedOperationException(); } /** * Renderers override this method to store the previous value of the associated component. * * @param component the target component to which the submitted value will be set * @param value the value to set */ protected void setSubmittedValue(UIComponent component, Object value) { // no-op unless overridden } /** * @param component the component of interest * @return true if this renderer should render an id attribute. */ protected boolean shouldWriteIdAttribute(UIComponent component) { // By default we only write the id attribute if: // // - We have a non-auto-generated id, or... // - We have client behaviors. // // We assume that if client behaviors are present, they // may need access to the id (AjaxBehavior certainly does). String id; return (null != (id = component.getId()) && (!id.startsWith(UIViewRoot.UNIQUE_ID_PREFIX) || ((component instanceof ClientBehaviorHolder) && !((ClientBehaviorHolder) component).getClientBehaviors().isEmpty()))); } protected String writeIdAttributeIfNecessary( FacesContext context, ResponseWriter writer, UIComponent component) { String id = null; if (shouldWriteIdAttribute(component)) { try { writer.writeAttribute("id", id = component.getClientId(context), "id"); } catch (IOException e) { if (logger.isLoggable(Level.WARNING)) { String message = MessageUtils.getExceptionMessageString( MessageUtils.CANT_WRITE_ID_ATTRIBUTE_ERROR_MESSAGE_ID, e.getMessage()); logger.warning(message); } } } return id; } protected void rendererParamsNotNull(FacesContext context, UIComponent component) { Util.notNull("context", context); Util.notNull("component", component); } protected boolean shouldEncode(UIComponent component) { // suppress rendering if "rendered" property on the component is // false. if (!component.isRendered()) { if (logger.isLoggable(Level.FINE)) { logger.log( Level.FINE, "End encoding component {0} since rendered attribute is set to false", component.getId()); } return false; } return true; } protected boolean shouldDecode(UIComponent component) { if (Util.componentIsDisabledOrReadonly(component)) { if (logger.isLoggable(Level.FINE)) { logger.log( Level.FINE, "No decoding necessary since the component {0} is disabled or read-only", component.getId()); } return false; } return true; } protected boolean shouldEncodeChildren(UIComponent component) { // suppress rendering if "rendered" property on the component is // false. if (!component.isRendered()) { if (logger.isLoggable(Level.FINE)) { logger.log( Level.FINE, "Children of component {0} will not be encoded since this component's rendered attribute is false", component.getId()); } return false; } return true; } /** * When rendering pass thru attributes, we need to take any attached Behaviors into account. The * presence of a non-empty Behaviors map can cause us to switch from optimized pass thru attribute * rendering to the unoptimized code path. However, in two very common cases - attaching action * behaviors to commands and attaching value change behaviors to editable value holders - the * behaviors map is populated with behaviors that are not handled by the pass thru attribute code * - ie. the behaviors are handled locally by the renderer. * * <p>In order to optimize such cases, we check to see whether the component's behaviors map * actually contains behaviors only for these non-pass thru attributes. If so, we can pass a null * behavior map into renderPassThruAttributes(), thus ensuring that we can take advantage of the * optimized pass thru rendering logic. * * <p>Note that in all cases where we use this method, we actually have two behavior events that * we want to check for - a low-level/dom event (eg. "click", or "change") plus a high-level * component event (eg. "action", or "valueChange"). * * @param component the component that we are rendering * @param domEventName the name of the dom-level event * @param componentEventName the name of the component-level event */ protected static Map<String, List<ClientBehavior>> getPassThruBehaviors( UIComponent component, String domEventName, String componentEventName) { if (!(component instanceof ClientBehaviorHolder)) { return null; } Map<String, List<ClientBehavior>> behaviors = ((ClientBehaviorHolder) component).getClientBehaviors(); int size = behaviors.size(); if ((size == 1) || (size == 2)) { boolean hasDomBehavior = behaviors.containsKey(domEventName); boolean hasComponentBehavior = behaviors.containsKey(componentEventName); // If the behavior map only contains behaviors for non-pass // thru attributes, return null. if (((size == 1) && (hasDomBehavior || hasComponentBehavior)) || ((size == 2) && hasDomBehavior && hasComponentBehavior)) { return null; } } return behaviors; } // --------------------------------------------------------- Private Methods /** * Recursively searches for {@link NamingContainer}s from the given start point looking for the * component with the <code>id</code> specified by <code>forComponent</code>. * * @param startPoint - the starting point in which to begin the search * @param forComponent - the component to search for * @return the component with the the <code>id</code that matches * <code> * forComponent</code> otheriwse null if no match is found. */ private static UIComponent findUIComponentBelow(UIComponent startPoint, String forComponent) { UIComponent retComp = null; if (startPoint.getChildCount() > 0) { List<UIComponent> children = startPoint.getChildren(); for (int i = 0, size = children.size(); i < size; i++) { UIComponent comp = children.get(i); if (comp instanceof NamingContainer) { try { retComp = comp.findComponent(forComponent); } catch (IllegalArgumentException iae) { continue; } } if (retComp == null) { if (comp.getChildCount() > 0) { retComp = findUIComponentBelow(comp, forComponent); } } if (retComp != null) { break; } } } return retComp; } // ----------------------------------------------------------- Inner Classes /** Simple class to encapsulate the name and value of a <code>UIParameter</code>. */ public static class Param { public String name; public String value; // -------------------------------------------------------- Constructors public Param(String name, String value) { this.name = name; this.value = value; } } /** * Structure to hold common info used by Select* components to reduce the number of times * component attributes are evaluated when rendering options. */ public static class OptionComponentInfo { String disabledClass; String enabledClass; String selectedClass; String unselectedClass; boolean disabled; boolean hideNoSelection; public OptionComponentInfo(UIComponent component) { Map<String, Object> attributes = component.getAttributes(); this.disabledClass = (String) attributes.get("disabledClass"); this.enabledClass = (String) attributes.get("enabledClass"); this.selectedClass = (String) attributes.get("selectedClass"); this.unselectedClass = (String) attributes.get("unselectedClass"); this.disabled = Util.componentIsDisabled(component); this.hideNoSelection = MenuRenderer.isHideNoSelection(component); } public String getDisabledClass() { return disabledClass; } public String getEnabledClass() { return enabledClass; } public boolean isDisabled() { return disabled; } public boolean isHideNoSelection() { return hideNoSelection; } public String getSelectedClass() { return selectedClass; } public String getUnselectedClass() { return unselectedClass; } } } // end of class HtmlBasicRenderer
/** A set of utilities for use in {@link RenderKit}s. */ public class RenderKitUtils { /** * The prefix to append to certain attributes when renderking <code>XHTML Transitional</code> * content. */ private static final String XHTML_ATTR_PREFIX = "xml:"; /** <code>Boolean</code> attributes to be rendered using <code>XHMTL</code> semantics. */ private static final String[] BOOLEAN_ATTRIBUTES = {"disabled", "ismap", "readonly"}; /** * An array of attributes that must be prefixed by {@link #XHTML_ATTR_PREFIX} when rendering * <code>XHTML Transitional</code> content. */ private static final String[] XHTML_PREFIX_ATTRIBUTES = {"lang"}; /** * The maximum number of array elements that can be used to hold content types from an accept * String. */ private static final int MAX_CONTENT_TYPES = 50; /** * The maximum number of content type parts. For example: for the type: "text/html; level=1; * q=0.5" The parts of this type would be: "text" - type "html; level=1" - subtype "0.5" - quality * value "1" - level value */ private static final int MAX_CONTENT_TYPE_PARTS = 4; /** The character that is used to delimit content types in an accept String. */ private static final String CONTENT_TYPE_DELIMITER = ","; /** * The character that is used to delimit the type and subtype portions of a content type in an * accept String. Example: text/html */ private static final String CONTENT_TYPE_SUBTYPE_DELIMITER = "/"; /** * JavaScript to be rendered when a commandLink is used. This may be expaned to include other * uses. */ private static final String SUN_JSF_JS = RIConstants.FACES_PREFIX + "sunJsfJs"; /** * This array represents the packages that can leverage the <code>attributesThatAreSet</code> List * for optimized attribute rendering. * * <p>IMPLEMENTATION NOTE: This must be kept in sync with the array in * UIComponentBase$AttributesMap and HtmlComponentGenerator. * * <p>Hopefully JSF 2.0 will remove the need for this. */ private static final String[] OPTIMIZED_PACKAGES = { "javax.faces.component", "javax.faces.component.html" }; static { // Sort the array for use with Arrays.binarySearch() Arrays.sort(OPTIMIZED_PACKAGES); } /** * IMPLEMENTATION NOTE: This must be kept in sync with the Key in UIComponentBase$AttributesMap * and HtmlComponentGenerator. * * <p>Hopefully JSF 2.0 will remove the need for this. */ private static final String ATTRIBUTES_THAT_ARE_SET_KEY = UIComponentBase.class.getName() + ".attributesThatAreSet"; protected static final Logger LOGGER = FacesLogger.RENDERKIT.getLogger(); // ------------------------------------------------------------ Constructors private RenderKitUtils() {} // ---------------------------------------------------------- Public Methods /** * Return the {@link RenderKit} for the current request. * * @param context the {@link FacesContext} of the current request * @return the {@link RenderKit} for the current request. */ public static RenderKit getCurrentRenderKit(FacesContext context) { RenderKitFactory renderKitFactory = (RenderKitFactory) FactoryFinder.getFactory(FactoryFinder.RENDER_KIT_FACTORY); return renderKitFactory.getRenderKit(context, context.getViewRoot().getRenderKitId()); } /** * Obtain and return the {@link ResponseStateManager} for the specified #renderKitId. * * @param context the {@link FacesContext} of the current request * @param renderKitId {@link RenderKit} ID * @return the {@link ResponseStateManager} for the specified #renderKitId * @throws FacesException if an exception occurs while trying to obtain the <code> * ResponseStateManager</code> */ public static ResponseStateManager getResponseStateManager( FacesContext context, String renderKitId) throws FacesException { assert (null != renderKitId); assert (null != context); RenderKit renderKit = context.getRenderKit(); if (renderKit == null) { // check request scope for a RenderKitFactory implementation RenderKitFactory factory = (RenderKitFactory) RequestStateManager.get(context, RequestStateManager.RENDER_KIT_IMPL_REQ); if (factory != null) { renderKit = factory.getRenderKit(context, renderKitId); } else { factory = (RenderKitFactory) FactoryFinder.getFactory(FactoryFinder.RENDER_KIT_FACTORY); if (factory == null) { throw new IllegalStateException(); } else { RequestStateManager.set(context, RequestStateManager.RENDER_KIT_IMPL_REQ, factory); } renderKit = factory.getRenderKit(context, renderKitId); } } return renderKit.getResponseStateManager(); } /** * Return a List of {@link javax.faces.model.SelectItem} instances representing the available * options for this component, assembled from the set of {@link * javax.faces.component.UISelectItem} and/or {@link javax.faces.component.UISelectItems} * components that are direct children of this component. If there are no such children, an empty * <code>Iterator</code> is returned. * * @param context The {@link javax.faces.context.FacesContext} for the current request. If null, * the UISelectItems behavior will not work. * @param component the component * @throws IllegalArgumentException if <code>context</code> is <code>null</code> * @return a List of the select items for the specified component */ public static List<SelectItem> getSelectItems(FacesContext context, UIComponent component) { if (context == null) { throw new IllegalArgumentException( MessageUtils.getExceptionMessageString( MessageUtils.NULL_PARAMETERS_ERROR_MESSAGE_ID, "context")); } ArrayList<SelectItem> list = new ArrayList<SelectItem>(); for (UIComponent kid : component.getChildren()) { if (kid instanceof UISelectItem) { UISelectItem item = (UISelectItem) kid; Object value = item.getValue(); if (value == null) { list.add( new SelectItem( item.getItemValue(), item.getItemLabel(), item.getItemDescription(), item.isItemDisabled(), item.isItemEscaped())); } else if (value instanceof SelectItem) { list.add((SelectItem) value); } else { throw new IllegalArgumentException( MessageUtils.getExceptionMessageString( MessageUtils.VALUE_NOT_SELECT_ITEM_ID, component.getId(), value.getClass().getName())); } } else if (kid instanceof UISelectItems) { Object value = ((UISelectItems) kid).getValue(); if (value instanceof SelectItem) { list.add((SelectItem) value); } else if (value instanceof SelectItem[]) { SelectItem[] items = (SelectItem[]) value; // we manually copy the elements so that the list is // modifiable. Arrays.asList() returns a non-mutable // list. //noinspection ManualArrayToCollectionCopy for (SelectItem item : items) { list.add(item); } } else if (value instanceof Collection) { for (Object element : ((Collection) value)) { if (SelectItem.class.isInstance(element)) { list.add((SelectItem) element); } else { throw new IllegalArgumentException( MessageUtils.getExceptionMessageString( MessageUtils.VALUE_NOT_SELECT_ITEM_ID, component.getId(), value.getClass().getName())); } } } else if (value instanceof Map) { Map optionMap = (Map) value; for (Object o : optionMap.entrySet()) { Entry entry = (Entry) o; Object key = entry.getKey(); Object val = entry.getValue(); if (key == null || val == null) { continue; } list.add(new SelectItem(val, key.toString())); } } else { throw new IllegalArgumentException( MessageUtils.getExceptionMessageString( MessageUtils.CHILD_NOT_OF_EXPECTED_TYPE_ID, "UISelectItem/UISelectItems", component.getFamily(), component.getId(), value != null ? value.getClass().getName() : "null")); } } } return (list); } /** * Render any "passthru" attributes, where we simply just output the raw name and value of the * attribute. This method is aware of the set of HTML4 attributes that fall into this bucket. * Examples are all the javascript attributes, alt, rows, cols, etc. * * @param writer writer the {@link javax.faces.context.ResponseWriter} to be used when writing the * attributes * @param component the component * @param attributes an array off attributes to be processed * @throws IOException if an error occurs writing the attributes */ public static void renderPassThruAttributes( ResponseWriter writer, UIComponent component, String[] attributes) throws IOException { assert (null != writer); assert (null != component); Map<String, Object> attrMap = component.getAttributes(); // PENDING - think anyone would run the RI using another implementation // of the jsf-api? If they did, then this would fall apart. That // scenario seems extremely unlikely. if (canBeOptimized(component)) { //noinspection unchecked List<String> setAttributes = (List<String>) component.getAttributes().get(ATTRIBUTES_THAT_ARE_SET_KEY); if (setAttributes != null) { renderPassThruAttributesOptimized(writer, component, attributes, setAttributes); } } else { // this block should only be hit by custom components leveraging // the RI's rendering code. We make no assumptions and loop through // all known attributes. boolean isXhtml = RIConstants.XHTML_CONTENT_TYPE.equals(writer.getContentType()); for (String attrName : attributes) { Object value = attrMap.get(attrName); if (value != null && shouldRenderAttribute(value)) { writer.writeAttribute(prefixAttribute(attrName, isXhtml), value, attrName); } } } } public static String prefixAttribute(final String attrName, final ResponseWriter writer) { return (prefixAttribute( attrName, RIConstants.XHTML_CONTENT_TYPE.equals(writer.getContentType()))); } public static String prefixAttribute(final String attrName, boolean isXhtml) { if (isXhtml) { if (Arrays.binarySearch(XHTML_PREFIX_ATTRIBUTES, attrName) > -1) { return XHTML_ATTR_PREFIX + attrName; } else { return attrName; } } else { return attrName; } } /** * Renders the attributes from {@link #BOOLEAN_ATTRIBUTES} using <code>XHMTL</code> semantics * (i.e., disabled="disabled"). * * @param writer writer the {@link ResponseWriter} to be used when writing the attributes * @param component the component * @throws IOException if an error occurs writing the attributes */ public static void renderXHTMLStyleBooleanAttributes(ResponseWriter writer, UIComponent component) throws IOException { assert (writer != null); assert (component != null); Map attrMap = component.getAttributes(); for (String attrName : BOOLEAN_ATTRIBUTES) { Object val = attrMap.get(attrName); if (val == null) { continue; } if (Boolean.valueOf(val.toString())) { writer.writeAttribute(attrName, true, attrName); } } } /** * Given an accept String from the client, and a <code>String</code> of server supported content * types, determine the best qualified content type for the client. If no match is found, or * either of the arguments are <code>null</code>, <code>null</code> is returned. * * @param accept The client accept String * @param serverSupportedTypes The types that the server supports * @param preferredType The preferred content type if another type is found with the same highest * quality factor. * @return The content type <code>String</code> */ public static String determineContentType( String accept, String serverSupportedTypes, String preferredType) { String contentType = null; if (null == accept || null == serverSupportedTypes) { return contentType; } String[][] clientContentTypes = buildTypeArrayFromString(accept); String[][] serverContentTypes = buildTypeArrayFromString(serverSupportedTypes); String[][] preferredContentType = buildTypeArrayFromString(preferredType); String[][] matchedInfo = findMatch(clientContentTypes, serverContentTypes, preferredContentType); // if best match exits and best match is not some wildcard, // return best match if ((matchedInfo[0][1] != null) && !(matchedInfo[0][2].equals("*"))) { contentType = matchedInfo[0][1] + CONTENT_TYPE_SUBTYPE_DELIMITER + matchedInfo[0][2]; } return contentType; } /** * @param contentType the content type in question * @return <code>true</code> if the content type is a known XML-based content type, otherwise, * <code>false</code> */ public static boolean isXml(String contentType) { return (RIConstants.XHTML_CONTENT_TYPE.equals(contentType) || RIConstants.APPLICATION_XML_CONTENT_TYPE.equals(contentType) || RIConstants.TEXT_XML_CONTENT_TYPE.equals(contentType)); } // --------------------------------------------------------- Private Methods /** * @param component the UIComponent in question * @return <code>true</code> if the component is within the <code>javax.faces.component</code> or * <code>javax.faces.component.html</code> packages, otherwise return <code>false</code> */ private static boolean canBeOptimized(UIComponent component) { Package p = component.getClass().getPackage(); return ((p != null) && (Arrays.binarySearch(OPTIMIZED_PACKAGES, p.getName()) >= 0)); } /** * For each attribute in <code>setAttributes</code>, perform a binary search against the array of * <code>knownAttributes</code> If a match is found and the value is not <code>null</code>, render * the attribute. * * @param writer the current writer * @param component the component whose attributes we're rendering * @param knownAttributes an array of pass-through attributes supported by this component * @param setAttributes a <code>List</code> of attributes that have been set on the provided * component * @throws IOException if an error occurs during the write */ private static void renderPassThruAttributesOptimized( ResponseWriter writer, UIComponent component, String[] knownAttributes, List<String> setAttributes) throws IOException { String[] attributes = setAttributes.toArray(new String[setAttributes.size()]); Arrays.sort(attributes); boolean isXhtml = RIConstants.XHTML_CONTENT_TYPE.equals(writer.getContentType()); Map<String, Object> attrMap = component.getAttributes(); for (String name : attributes) { if (Arrays.binarySearch(knownAttributes, name) >= 0) { Object value = attrMap.get(name); if (value != null && shouldRenderAttribute(value)) { writer.writeAttribute(prefixAttribute(name, isXhtml), value, name); } } } } /** * Determines if an attribute should be rendered based on the specified #attributeVal. * * @param attributeVal the attribute value * @return <code>true</code> if and only if #attributeVal is an instance of a wrapper for a * primitive type and its value is equal to the default value for that type as given in the * specification. */ private static boolean shouldRenderAttribute(Object attributeVal) { if (attributeVal instanceof String) { return true; } else if (attributeVal instanceof Boolean && Boolean.FALSE.equals(attributeVal)) { return false; } else if (attributeVal instanceof Integer && (Integer) attributeVal == Integer.MIN_VALUE) { return false; } else if (attributeVal instanceof Double && (Double) attributeVal == Double.MIN_VALUE) { return false; } else if (attributeVal instanceof Character && (Character) attributeVal == Character.MIN_VALUE) { return false; } else if (attributeVal instanceof Float && (Float) attributeVal == Float.MIN_VALUE) { return false; } else if (attributeVal instanceof Short && (Short) attributeVal == Short.MIN_VALUE) { return false; } else if (attributeVal instanceof Byte && (Byte) attributeVal == Byte.MIN_VALUE) { return false; } else if (attributeVal instanceof Long && (Long) attributeVal == Long.MIN_VALUE) { return false; } return true; } /** * This method builds a two element array structure as follows: Example: Given the following * accept string: text/html; level=1, text/plain; q=0.5 [0][0] 1 (quality is 1 if none specified) * [0][1] "text" (type) [0][2] "html; level=1" (subtype) [0][3] 1 (level, if specified; null if * not) * * <p>[1][0] .5 [1][1] "text" [1][2] "plain" [1][3] (level, if specified; null if not) * * <p>The array is used for comparison purposes in the findMatch method. * * @param accept An accept <code>String</code> * @return an two dimensional array containing content-type/quality info */ private static String[][] buildTypeArrayFromString(String accept) { String[][] arrayAccept = new String[MAX_CONTENT_TYPES][MAX_CONTENT_TYPE_PARTS]; // return if empty if ((accept == null) || (accept.length() == 0)) return arrayAccept; // some helper variables StringBuilder typeSubType; String type; String subtype; String level = null; String quality = null; // Parse "types" String[] types = Util.split(accept, CONTENT_TYPE_DELIMITER); int index = -1; for (int i = 0; i < types.length; i++) { String token = types[i].trim(); index += 1; // Check to see if our accept string contains the delimiter that is used // to add uniqueness to a type/subtype, and/or delimits a qualifier value: // Example: text/html;level=1,text/html;level=2; q=.5 if (token.contains(";")) { String[] typeParts = Util.split(token, ";"); typeSubType = new StringBuilder(typeParts[0].trim()); for (int j = 1; j < typeParts.length; j++) { quality = "not set"; token = typeParts[j].trim(); // if "level" is present, make sure it gets included in the "type/subtype" if (token.contains("level")) { typeSubType.append(';').append(token); String[] levelParts = Util.split(token, "="); level = levelParts[0].trim(); if (level.equalsIgnoreCase("level")) { level = levelParts[1].trim(); } } else { quality = token; String[] qualityParts = Util.split(quality, "="); quality = qualityParts[0].trim(); if (quality.equalsIgnoreCase("q")) { quality = qualityParts[1].trim(); break; } else { quality = "not set"; // to identifiy that no quality was supplied } } } } else { typeSubType = new StringBuilder(token); quality = "not set"; // to identifiy that no quality was supplied } // now split type and subtype if (typeSubType.indexOf(CONTENT_TYPE_SUBTYPE_DELIMITER) >= 0) { String[] typeSubTypeParts = Util.split(typeSubType.toString(), CONTENT_TYPE_SUBTYPE_DELIMITER); type = typeSubTypeParts[0].trim(); subtype = typeSubTypeParts[1].trim(); } else { type = typeSubType.toString(); subtype = ""; } // check quality and assign values if ("not set".equals(quality)) { if (type.equals("*") && subtype.equals("*")) { quality = "0.01"; } else if (!type.equals("*") && subtype.equals("*")) { quality = "0.02"; } else if (type.equals("*") && subtype.length() == 0) { quality = "0.01"; } else { quality = "1"; } } arrayAccept[index][0] = quality; arrayAccept[index][1] = type; arrayAccept[index][2] = subtype; arrayAccept[index][3] = level; } return (arrayAccept); } /** * For each server supported type, compare client (browser) specified types. If a match is found, * keep track of the highest quality factor. The end result is that for all matches, only the one * with the highest quality will be returned. * * @param clientContentTypes An <code>array</code> of accept <code>String</code> information for * the client built from @{link #buildTypeArrayFromString}. * @param serverSupportedContentTypes An <code>array</code> of accept <code>String</code> * information for the server supported types built from @{link #buildTypeArrayFromString}. * @param preferredContentType An <code>array</code> of preferred content type information. * @return An <code>array</code> containing the parts of the preferred content type for the * client. The information is stored as outlined in @{link #buildTypeArrayFromString}. */ private static String[][] findMatch( String[][] clientContentTypes, String[][] serverSupportedContentTypes, String[][] preferredContentType) { // result array String[][] results = new String[MAX_CONTENT_TYPES][MAX_CONTENT_TYPE_PARTS]; int resultidx = -1; // the highest quality double highestQFactor = 0; // the record with the highest quality int idx = 0; for (int sidx = 0; sidx < MAX_CONTENT_TYPES; sidx++) { // get server type String serverType = serverSupportedContentTypes[sidx][1]; if (serverType != null) { for (int cidx = 0; cidx < MAX_CONTENT_TYPES; cidx++) { // get browser type String browserType = clientContentTypes[cidx][1]; if (browserType != null) { // compare them and check for wildcard if ((browserType.equalsIgnoreCase(serverType)) || (browserType.equals("*"))) { // types are equal or browser type is wildcard - compare subtypes if ((clientContentTypes[cidx][2].equalsIgnoreCase( serverSupportedContentTypes[sidx][2])) || (clientContentTypes[cidx][2].equals("*"))) { // subtypes are equal or browser subtype is wildcard // found match: multiplicate qualities and add to result array // if there was a level associated, this gets higher precedence, so // factor in the level in the calculation. double cLevel = 0.0; double sLevel = 0.0; if (clientContentTypes[cidx][3] != null) { cLevel = (Double.parseDouble(clientContentTypes[cidx][3])) * .10; } if (serverSupportedContentTypes[sidx][3] != null) { sLevel = (Double.parseDouble(serverSupportedContentTypes[sidx][3])) * .10; } double cQfactor = Double.parseDouble(clientContentTypes[cidx][0]) + cLevel; double sQfactor = Double.parseDouble(serverSupportedContentTypes[sidx][0]) + sLevel; double resultQuality = cQfactor * sQfactor; resultidx += 1; results[resultidx][0] = String.valueOf(resultQuality); if (clientContentTypes[cidx][2].equals("*")) { // browser subtype is wildcard // return type and subtype (wildcard) results[resultidx][1] = clientContentTypes[cidx][1]; results[resultidx][2] = clientContentTypes[cidx][2]; } else { // return server type and subtype results[resultidx][1] = serverSupportedContentTypes[sidx][1]; results[resultidx][2] = serverSupportedContentTypes[sidx][2]; results[resultidx][3] = serverSupportedContentTypes[sidx][3]; } // check if this was the highest factor if (resultQuality > highestQFactor) { idx = resultidx; highestQFactor = resultQuality; } } } } } } } // First, determine if we have a type that has the highest quality factor that // also matches the preferred type (if there is one): String[][] match = new String[1][3]; if (preferredContentType[0][0] != null) { BigDecimal highestQual = BigDecimal.valueOf(highestQFactor); for (int i = 0; i <= resultidx; i++) { if ((BigDecimal.valueOf(Double.parseDouble(results[i][0])).compareTo(highestQual) == 0) && (results[i][1]).equals(preferredContentType[0][1]) && (results[i][2]).equals(preferredContentType[0][2])) { match[0][0] = results[i][0]; match[0][1] = results[i][1]; match[0][2] = results[i][2]; return match; } } } match[0][0] = results[idx][0]; match[0][1] = results[idx][1]; match[0][2] = results[idx][2]; return match; } /** * Replaces all occurrences of <code>-</code> with <code>$_</code>. * * @param origIdentifier the original identifer that needs to be 'ECMA-ized' * @return an ECMA valid identifer */ public static String createValidECMAIdentifier(String origIdentifier) { return origIdentifier.replace("-", "$_"); } /** * Renders the Javascript necessary to add and remove request parameters to the current form. * * @param writer the <code>ResponseWriter</code> * @param context the <code>FacesContext</code> for the current request * @throws java.io.IOException if an error occurs writing to the response */ public static void renderFormInitScript(ResponseWriter writer, FacesContext context) throws IOException { WebConfiguration webConfig = WebConfiguration.getInstance(context.getExternalContext()); if (webConfig.isOptionEnabled(BooleanWebContextInitParameter.ExternalizeJavaScript)) { // PENDING // We need to look into how to make this work in a portlet environment. // For the time being, this feature will need to be disabled when running // in a portlet. /* String mapping = Util.getFacesMapping(context); String uri; if ((mapping != null) && (Util.isPrefixMapped(mapping))) { uri = mapping + '/' + RIConstants.SUN_JSF_JS_URI; } else { uri = '/' + RIConstants.SUN_JSF_JS_URI + mapping; } writer.write('\n'); writer.startElement("script", null); writer.writeAttribute("type", "text/javascript", null); writer.writeAttribute("src", context.getExternalContext() .getRequestContextPath() + uri, null); writer.endElement("script"); writer.write("\n"); */ ResourceHandler handler = context.getApplication().getResourceHandler(); Resource resource = handler.createResource("scripts/sunjsf.js", "jsfri"); writer.write('\n'); writer.startElement("script", null); writer.writeAttribute("type", "text/javascript", null); writer.writeAttribute("src", ((resource != null) ? resource.getRequestPath() : ""), null); writer.endElement("script"); } else { writer.write('\n'); writer.startElement("script", null); writer.writeAttribute("type", "text/javascript", null); writer.writeAttribute("language", "Javascript", null); writeSunJS(context, writer); writer.endElement("script"); writer.write("\n"); } } /** * Returns a string that can be inserted into the <code>onclick</code> handler of a command. This * string will add all request parameters as well as the client ID of the activated command to the * form as hidden input parameters, update the target of the link if necessary, and handle the * form submission. The content of {@link #SUN_JSF_JS} must be rendered prior to using this * method. * * @param formClientId the client ID of the form * @param commandClientId the client ID of the command * @param target the link target * @param params the nested parameters, if any @return a String suitable for the <code>onclick * </code> handler of a command * @return the default <code>onclick</code> JavaScript for the default command link component */ public static String getCommandLinkOnClickScript( String formClientId, String commandClientId, String target, Param[] params) { StringBuilder sb = new StringBuilder(256); sb.append("if(typeof jsfcljs == 'function'){jsfcljs(document.getElementById('"); sb.append(formClientId); sb.append("'),{'"); sb.append(commandClientId).append("':'").append(commandClientId); for (Param param : params) { String pn = param.name; if (pn != null && pn.length() != 0) { String pv = param.value; sb.append("','"); sb.append(pn.replace("'", "\\\'")); sb.append("':'"); if (pv != null && pv.length() != 0) { sb.append(pv.replace("'", "\\\'")); } } } sb.append("'},'"); sb.append(target); sb.append("');}return false"); return sb.toString(); } /** * This is a utility method for compressing multi-lined javascript. In the case of {@link * #SUN_JSF_JS} it offers about a 47% decrease in length. * * <p>For our purposes, compression is just trimming each line and then writing it out. It's * pretty simplistic, but it works. * * @param JSString the string to compress * @return the compressed string */ public static char[] compressJS(String JSString) { BufferedReader reader = new BufferedReader(new StringReader(JSString)); StringWriter writer = new StringWriter(1024); try { for (String line = reader.readLine(); line != null; line = reader.readLine()) { line = line.trim(); writer.write(line); } return writer.toString().toCharArray(); } catch (IOException ioe) { // won't happen } return null; } /** * Return the implementation JavaScript. If compression is enabled, the result will be compressed. * * @param context - the <code>FacesContext</code> for the current request * @param writer - the <code>Writer</code> to write the JS to * @throws IOException if the JavaScript cannot be written */ public static void writeSunJS(FacesContext context, Writer writer) throws IOException { writer.write((char[]) context.getExternalContext().getApplicationMap().get(SUN_JSF_JS)); } // --------------------------------------------------------- Private Methods /** * Loads the contents of the sunjsf.js file into memory removing any comments/empty lines it * encoutners, and, if enabled, compressing the result. This method should only be called when the * application is being initialized. * * @param extContext the ExternalContext for this application */ public static synchronized void loadSunJsfJs(ExternalContext extContext) { Map<String, Object> appMap = extContext.getApplicationMap(); char[] sunJsfJs; BufferedReader reader = null; try { // Don't use Util.getCurrentLoader(). This JS resource should // be available from the same classloader that loaded RenderKitUtils. // Doing so allows us to be more OSGi friendly. URL url = RenderKitUtils.class.getClassLoader().getResource("com/sun/faces/sunjsf.js"); if (url == null) { LOGGER.severe("jsf.renderkit.util.cannot_load_js"); return; } URLConnection conn = url.openConnection(); conn.setUseCaches(false); InputStream input = conn.getInputStream(); reader = new BufferedReader(new InputStreamReader(input)); StringBuilder builder = new StringBuilder(128); for (String line = reader.readLine(); line != null; line = reader.readLine()) { String temp = line.trim(); if (temp.length() == 0 || temp.startsWith("/*") || temp.startsWith("*") || temp.startsWith("*/") || temp.startsWith("//")) { continue; } builder.append(line).append('\n'); } builder.deleteCharAt(builder.length() - 1); if (WebConfiguration.getInstance(extContext) .isOptionEnabled(BooleanWebContextInitParameter.CompressJavaScript)) { sunJsfJs = compressJS(builder.toString()); } else { sunJsfJs = builder.toString().toCharArray(); } appMap.put(SUN_JSF_JS, sunJsfJs); } catch (IOException ioe) { LOGGER.log(Level.SEVERE, "jsf.renderkit.util.cannot_load_js", ioe); } finally { if (reader != null) { try { reader.close(); } catch (IOException ioe) { // ignore } } } } } // END RenderKitUtils
public class AjaxBehaviorRenderer extends ClientBehaviorRenderer { // Log instance for this class protected static final Logger logger = FacesLogger.RENDERKIT.getLogger(); /** Flag determining whether or not javax.faces.ViewState should be namespaced. */ protected transient boolean namespaceParameters; public AjaxBehaviorRenderer() { WebConfiguration webConfig = WebConfiguration.getInstance(); namespaceParameters = webConfig.isOptionEnabled(BooleanWebContextInitParameter.NamespaceParameters); } // ------------------------------------------------------ Rendering Methods @Override public String getScript(ClientBehaviorContext behaviorContext, ClientBehavior behavior) { if (!(behavior instanceof AjaxBehavior)) { // TODO: use MessageUtils for this error message? throw new IllegalArgumentException( "Instance of javax.faces.component.behavior.AjaxBehavior required: " + behavior); } if (((AjaxBehavior) behavior).isDisabled()) { return null; } return buildAjaxCommand(behaviorContext, (AjaxBehavior) behavior, namespaceParameters); } @Override public void decode(FacesContext context, UIComponent component, ClientBehavior behavior) { if (null == context || null == component || null == behavior) { throw new NullPointerException(); } if (!(behavior instanceof AjaxBehavior)) { // TODO: use MessageUtils for this error message? throw new IllegalArgumentException( "Instance of javax.faces.component.behavior.AjaxBehavior required: " + behavior); } AjaxBehavior ajaxBehavior = (AjaxBehavior) behavior; // First things first - if AjaxBehavior is disabled, we are done. if (ajaxBehavior.isDisabled()) { return; } component.queueEvent(createEvent(context, component, ajaxBehavior)); if (logger.isLoggable(Level.FINE)) { logger.fine("This command resulted in form submission " + " AjaxBehaviorEvent queued."); logger.log(Level.FINE, "End decoding component {0}", component.getId()); } } // Creates an AjaxBehaviorEvent for the specified component/behavior private static AjaxBehaviorEvent createEvent( FacesContext facesContext, UIComponent component, AjaxBehavior ajaxBehavior) { AjaxBehaviorEvent event = new AjaxBehaviorEvent(facesContext, component, ajaxBehavior); PhaseId phaseId = isImmediate(component, ajaxBehavior) ? PhaseId.APPLY_REQUEST_VALUES : PhaseId.INVOKE_APPLICATION; event.setPhaseId(phaseId); return event; } // Tests whether we should perform immediate processing. Note // that we "inherit" immediate from the parent if not specified // on the behavior. private static boolean isImmediate(UIComponent component, AjaxBehavior ajaxBehavior) { boolean immediate = false; if (ajaxBehavior.isImmediateSet()) { immediate = ajaxBehavior.isImmediate(); } else if (component instanceof EditableValueHolder) { immediate = ((EditableValueHolder) component).isImmediate(); } else if (component instanceof ActionSource) { immediate = ((ActionSource) component).isImmediate(); } return immediate; } private static String buildAjaxCommand( ClientBehaviorContext behaviorContext, AjaxBehavior ajaxBehavior, boolean namespaceParameters) { // First things first - if AjaxBehavior is disabled, we are done. if (ajaxBehavior.isDisabled()) { return null; } UIComponent component = behaviorContext.getComponent(); String eventName = behaviorContext.getEventName(); StringBuilder ajaxCommand = new StringBuilder(256); Collection<String> execute = ajaxBehavior.getExecute(); Collection<String> render = ajaxBehavior.getRender(); String onevent = ajaxBehavior.getOnevent(); String onerror = ajaxBehavior.getOnerror(); String sourceId = behaviorContext.getSourceId(); String delay = ajaxBehavior.getDelay(); Boolean resetValues = null; if (ajaxBehavior.isResetValuesSet()) { resetValues = ajaxBehavior.isResetValues(); } Collection<ClientBehaviorContext.Parameter> params = behaviorContext.getParameters(); // Needed workaround for SelectManyCheckbox - if execute doesn't have sourceId, // we need to add it - otherwise, we use the default, which is sourceId:child, which // won't work. ClientBehaviorContext.Parameter foundparam = null; for (ClientBehaviorContext.Parameter param : params) { if (param.getName().equals("incExec") && (Boolean) param.getValue()) { foundparam = param; } } if (foundparam != null && !execute.contains(sourceId)) { execute = new LinkedList<>(execute); execute.add(component.getClientId()); } if (foundparam != null) { try { // And since this is a hack, we now try to remove the param params.remove(foundparam); } catch (UnsupportedOperationException uoe) { if (logger.isLoggable(Level.FINEST)) { logger.log(Level.FINEST, "Unsupported operation", uoe); } } } ajaxCommand.append("mojarra.ab("); if (sourceId == null) { ajaxCommand.append("this"); } else { ajaxCommand.append("'"); ajaxCommand.append(sourceId); ajaxCommand.append("'"); } ajaxCommand.append(",event,'"); ajaxCommand.append(eventName); ajaxCommand.append("',"); appendIds(component, ajaxCommand, execute); ajaxCommand.append(","); appendIds(component, ajaxCommand, render); String namingContainerId = null; if (namespaceParameters) { FacesContext context = behaviorContext.getFacesContext(); UIViewRoot viewRoot = context.getViewRoot(); if (viewRoot instanceof NamingContainer) { namingContainerId = viewRoot.getContainerClientId(context); } } if ((namingContainerId != null) || (onevent != null) || (onerror != null) || (delay != null) || (resetValues != null) || !params.isEmpty()) { ajaxCommand.append(",{"); if (namingContainerId != null) { // the literal string must exactly match the corresponding value // in jsf.js. RenderKitUtils.appendProperty( ajaxCommand, "com.sun.faces.namingContainerId", namingContainerId, true); } if (onevent != null) { RenderKitUtils.appendProperty(ajaxCommand, "onevent", onevent, false); } if (onerror != null) { RenderKitUtils.appendProperty(ajaxCommand, "onerror", onerror, false); } if (delay != null) { RenderKitUtils.appendProperty(ajaxCommand, "delay", delay, true); } if (resetValues != null) { RenderKitUtils.appendProperty(ajaxCommand, "resetValues", resetValues, false); } if (!params.isEmpty()) { for (ClientBehaviorContext.Parameter param : params) { RenderKitUtils.appendProperty(ajaxCommand, param.getName(), param.getValue()); } } ajaxCommand.append("}"); } ajaxCommand.append(")"); return ajaxCommand.toString(); } // Appends an ids argument to the ajax command private static void appendIds( UIComponent component, StringBuilder builder, Collection<String> ids) { if ((null == ids) || ids.isEmpty()) { builder.append('0'); return; } builder.append("'"); boolean first = true; for (String id : ids) { if (id.trim().length() == 0) { continue; } if (!first) { builder.append(' '); } else { first = false; } if (id.equals("@all") || id.equals("@none") || id.equals("@form") || id.equals("@this")) { builder.append(id); } else { builder.append(getResolvedId(component, id)); } } builder.append("'"); } // Returns the resolved (client id) for a particular id. private static String getResolvedId(UIComponent component, String id) { UIComponent resolvedComponent = component.findComponent(id); if (resolvedComponent == null) { if (id.charAt(0) == UINamingContainer.getSeparatorChar(FacesContext.getCurrentInstance())) { return id.substring(1); } return id; } return resolvedComponent.getClientId(); } }