Package org.omnifaces.resourcehandler

Source Code of org.omnifaces.resourcehandler.GraphicResource

/*
* Copyright 2014 OmniFaces.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
*     http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*/
package org.omnifaces.resourcehandler;

import static java.util.Arrays.asList;
import static java.util.Collections.singletonMap;
import static org.omnifaces.util.Faces.getContext;
import static org.omnifaces.util.Servlets.toQueryString;
import static org.omnifaces.util.Utils.coalesce;
import static org.omnifaces.util.Utils.isEmpty;
import static org.omnifaces.util.Utils.isNumber;
import static org.omnifaces.util.Utils.isOneAnnotationPresent;
import static org.omnifaces.util.Utils.toByteArray;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;

import javax.el.ValueExpression;
import javax.faces.FacesException;
import javax.faces.application.Application;
import javax.faces.application.Resource;
import javax.faces.context.FacesContext;
import javax.faces.convert.Converter;
import javax.xml.bind.DatatypeConverter;

import org.omnifaces.component.output.GraphicImage;
import org.omnifaces.el.ExpressionInspector;
import org.omnifaces.el.MethodReference;

/**
* <p>
* This {@link Resource} implementation is used by the {@link GraphicImage} component.
*
* @author Bauke Scholtz
* @since 2.0
*/
public class GraphicResource extends DynamicResource {

  // Constants ------------------------------------------------------------------------------------------------------

  private static final String DEFAULT_CONTENT_TYPE = "image";
  private static final Map<String, String> CONTENT_TYPES_BY_BASE64_HEADER = createContentTypesByBase64Header();
  private static final Map<String, MethodReference> ALLOWED_METHODS = new HashMap<>();
  private static final GraphicImage DUMMY_COMPONENT = new GraphicImage();
  private static final String[] EMPTY_PARAMS = new String[0];

  @SuppressWarnings("unchecked")
  private static final Class<? extends Annotation>[] REQUIRED_ANNOTATION_TYPES = new Class[] {
    javax.faces.bean.ApplicationScoped.class, javax.enterprise.context.ApplicationScoped.class
  };

  private static final String ERROR_INVALID_LASTMODIFIED =
    "o:graphicImage 'lastModified' attribute must be an instance of Long or Date."
      + " Encountered an invalid value of '%s'.";
  private static final String ERROR_UNKNOWN_METHOD =
    "o:graphicImage 'value' attribute must refer an existing method."
      + " Encountered an unknown method of '%s'.";
  private static final String ERROR_INVALID_SCOPE =
    "o:graphicImage 'value' attribute must refer an @ApplicationScoped bean."
      + " Cannot find the right annotation on bean class '%s'.";
  private static final String ERROR_INVALID_RETURNTYPE =
    "o:graphicImage 'value' attribute must represent a method returning an InputStream or byte[]."
      + " Encountered an invalid return value of '%s'.";
  private static final String ERROR_INVALID_PARAMS =
    "o:graphicImage 'value' attribute must specify valid method parameters."
      + " Encountered invalid method parameters '%s'.";

  private static final Map<String, String> createContentTypesByBase64Header() {
    Map<String, String> contentTypesByBase64Header = new HashMap<>();
    contentTypesByBase64Header.put("/9j/", "image/jpeg");
    contentTypesByBase64Header.put("iVBORw", "image/png");
    contentTypesByBase64Header.put("R0lGOD", "image/gif");
    contentTypesByBase64Header.put("AAABAA", "image/x-icon");

    // BMP and TIFF are unlikely used as web image formats due to ineffective large sizes.
    // contentTypesByBase64Header.put("Qk0", "image/bmp");
    // contentTypesByBase64Header.put("SUkqAA", "image/tiff");
    // contentTypesByBase64Header.put("TU0AKg", "image/tiff");

    return Collections.unmodifiableMap(contentTypesByBase64Header);
  }

  // Variables ------------------------------------------------------------------------------------------------------

  private Object content;
  private String[] params;

  // Constructors ---------------------------------------------------------------------------------------------------

  /**
   * Construct a new graphic resource which uses the given content as data URI.
   * @param content The graphic resource content, to be represented as data URI.
   * @param contentType The graphic resource content type. If this is <code>null</code>, then it will be guessed
   * based on the content type signature in the content header. So far, JPEG, PNG, GIF and ICO are supported.
   */
  public GraphicResource(Object content, String contentType) {
    super("", GraphicResourceHandler.LIBRARY_NAME, contentType);
    this.content = content;
  }

  /**
   * Construct a new graphic resource based on the given name, EL method parameters converted as string, and the
   * "last modified" representation.
   * @param name The graphic resource name, usually representing the base and method of EL method expression.
   * @param params The graphic resource method parameters.
   * @param lastModified The "last modified" representation of the graphic resource, can be {@link Long} or
   * {@link Date}, or otherwise an attempt will be made to parse it as {@link Long}.
   * @throws IllegalArgumentException If "last modified" can not be parsed to a timestamp.
   */
  public GraphicResource(String name, String[] params, Object lastModified) {
    super(name, GraphicResourceHandler.LIBRARY_NAME, DEFAULT_CONTENT_TYPE);
    this.params = coalesce(params, EMPTY_PARAMS);

    if (lastModified instanceof Long) {
      setLastModified((Long) lastModified);
    }
    else if (lastModified instanceof Date) {
      setLastModified(((Date) lastModified).getTime());
    }
    else if (isNumber(String.valueOf(lastModified))) {
      setLastModified(Long.valueOf(lastModified.toString()));
    }
    else if (lastModified != null) {
      throw new IllegalArgumentException(String.format(ERROR_INVALID_LASTMODIFIED, lastModified));
    }
  }

  /**
   * Create a new graphic resource based on the given value expression.
   * This is called by {@link GraphicImage} component.
   * @param context The involved faces context.
   * @param value The value expression representing content to create a new graphic resource for.
   * @param lastModified The "last modified" representation of the graphic resource, can be {@link Long} or
   * {@link Date}, or otherwise an attempt will be made to parse it as {@link Long}.
   * @return The new graphic resource.
   * @throws IllegalArgumentException When the "value" attribute of the given component is absent or does not
   * represent a method expression referring an existing method taking at least one argument.
   */
  public static GraphicResource create(FacesContext context, ValueExpression value, Object lastModified) {
    MethodReference methodReference = ExpressionInspector.getMethodReference(context.getELContext(), value);

    if (methodReference.getMethod() == null) {
      throw new IllegalArgumentException(String.format(ERROR_UNKNOWN_METHOD, value.getExpressionString()));
    }

    String name = getResourceName(methodReference);

    if (!ALLOWED_METHODS.containsKey(name)) { // No need to validate everytime when already known.
      Class<? extends Object> beanClass = methodReference.getBase().getClass();

      if (!isOneAnnotationPresent(beanClass, REQUIRED_ANNOTATION_TYPES)) {
        throw new IllegalArgumentException(String.format(ERROR_INVALID_SCOPE, beanClass));
      }

      ALLOWED_METHODS.put(name, new MethodReference(methodReference.getBase(), methodReference.getMethod()));
    }

    Object[] params = methodReference.getActualParameters();
    String[] convertedParams = convertToStrings(context, params, methodReference.getMethod().getParameterTypes());
    return new GraphicResource(name, convertedParams, lastModified);
  }

  /**
   * An override which either returns the data URI or appends the converted method parameters to the query string.
   */
  @Override
  public String getRequestPath() {
    if (content != null) {
      return getDataURI();
    }
    else {
      String queryString = isEmpty(params) ? "" : ("&" + toQueryString(singletonMap("p", asList(params))));
      return super.getRequestPath() + queryString;
    }
  }

  /**
   * Returns the data URI for resource's content.
   * @return The data URI for resource's content.
   */
  protected String getDataURI() {
    byte[] bytes;

    if (content instanceof InputStream) {
      try {
        bytes = toByteArray((InputStream) content);
      }
      catch (IOException e) {
        throw new FacesException(e);
      }
    }
    else if (content instanceof byte[]) {
      bytes = (byte[]) content;
    }
    else {
      throw new IllegalArgumentException(String.format(ERROR_INVALID_RETURNTYPE, content));
    }

    String base64 = DatatypeConverter.printBase64Binary(bytes);
    String contentType = getContentType();

    if (contentType == null) {
      contentType = guessContentType(base64);
    }

    return "data:" + contentType + ";base64," + base64;
  }

  @Override
  public InputStream getInputStream() throws IOException {
    MethodReference methodReference = ALLOWED_METHODS.get(getResourceName().split("\\.", 2)[0]);

    if (methodReference == null) {
      return null; // Ignore hacker attempts. I'd rather return 400 here, but JSF spec doesn't support it.
    }

    Method method = methodReference.getMethod();
    Object[] convertedParams = convertToObjects(getContext(), params, method.getParameterTypes());
    Object content;

    try {
      content = method.invoke(methodReference.getBase(), convertedParams);
    }
    catch (Exception e) {
      throw new FacesException(e);
    }

    if (content instanceof InputStream) {
      return (InputStream) content;
    }
    else if (content instanceof byte[]) {
      return new ByteArrayInputStream((byte[]) content);
    }
    else {
      throw new IllegalArgumentException(String.format(ERROR_INVALID_RETURNTYPE, content));
    }
  }

  // Helpers --------------------------------------------------------------------------------------------------------

  /**
   * This must return an unique and URL-safe identifier of the bean+method without any periods.
   */
  private static String getResourceName(MethodReference methodReference) {
    return methodReference.getBase().getClass().getSimpleName() + "_" + methodReference.getMethod().getName();
  }

  /**
   * Guess the image content type based on given base64 encoded content for data URI.
   */
  private static String guessContentType(String base64) {
    for (Entry<String, String> contentTypeByBase64Header : CONTENT_TYPES_BY_BASE64_HEADER.entrySet()) {
      if (base64.startsWith(contentTypeByBase64Header.getKey())) {
        return contentTypeByBase64Header.getValue();
      }
    }

    return DEFAULT_CONTENT_TYPE;
  }

  /**
   * Convert the given objects to strings using converters registered on given types.
   * @throws IllegalArgumentException When the length of given params doesn't match those of given types.
   */
  private static String[] convertToStrings(FacesContext context, Object[] values, Class<?>[] types) {
    validateParamLength(values, types);
    String[] strings = new String[values.length];
    Application application = context.getApplication();

    for (int i = 0; i < values.length; i++) {
      Object value = values[i];
      Converter converter = application.createConverter(types[i]);
      strings[i] = (converter != null)
        ? converter.getAsString(context, DUMMY_COMPONENT, value)
        : (value != null) ? value.toString() : "";
    }

    return strings;
  }

  /**
   * Convert the given strings to objects using converters registered on given types.
   * @throws IllegalArgumentException When the length of given params doesn't match those of given types.
   */
  private static Object[] convertToObjects(FacesContext context, String[] values, Class<?>[] types) {
    validateParamLength(values, types);
    Object[] objects = new Object[values.length];
    Application application = context.getApplication();

    for (int i = 0; i < values.length; i++) {
      String value = isEmpty(values[i]) ? null : values[i];
      Converter converter = application.createConverter(types[i]);
      objects[i] = (converter != null)
        ? converter.getAsObject(context, DUMMY_COMPONENT, value)
        : value;
    }

    return objects;
  }

  private static void validateParamLength(Object[] params, Class<?>[] types) {
    if (params.length != types.length) {
      throw new IllegalArgumentException(String.format(ERROR_INVALID_PARAMS, Arrays.toString(params)));
    }
  }

}
TOP

Related Classes of org.omnifaces.resourcehandler.GraphicResource

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.