Beispiel #1
0
/**
 * 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
Beispiel #3
0
/** 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();
  }
}