All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.omnifaces.component.output.GraphicImage Maven / Gradle / Ivy

There is a newer version: 4.5.1
Show newest version
/*
 * Copyright 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
 *
 *     https://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.component.output;

import static org.omnifaces.config.OmniFaces.OMNIFACES_LIBRARY_NAME;
import static org.omnifaces.config.OmniFaces.OMNIFACES_SCRIPT_NAME;
import static org.omnifaces.resourcehandler.DefaultResourceHandler.RES_NOT_FOUND;
import static org.omnifaces.util.Components.VALUE_ATTRIBUTE;
import static org.omnifaces.util.FacesLocal.createResource;
import static org.omnifaces.util.Renderers.writeAttributes;
import static org.omnifaces.util.Renderers.writeIdAttributeIfNecessary;
import static org.omnifaces.util.Utils.coalesce;
import static org.omnifaces.util.Utils.isEmpty;

import java.io.IOException;
import java.io.InputStream;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

import jakarta.el.ValueExpression;
import jakarta.faces.application.Application;
import jakarta.faces.application.Resource;
import jakarta.faces.application.ResourceDependency;
import jakarta.faces.component.FacesComponent;
import jakarta.faces.component.html.HtmlGraphicImage;
import jakarta.faces.context.FacesContext;
import jakarta.faces.context.ResponseWriter;

import org.omnifaces.cdi.GraphicImageBean;
import org.omnifaces.el.ExpressionInspector;
import org.omnifaces.el.MethodReference;
import org.omnifaces.resourcehandler.DefaultResourceHandler;
import org.omnifaces.resourcehandler.DynamicResource;
import org.omnifaces.resourcehandler.GraphicResource;
import org.omnifaces.resourcehandler.GraphicResourceHandler;
import org.omnifaces.util.Faces;
import org.omnifaces.util.State;

/**
 * 

* The <o:graphicImage> is a component that extends the standard <h:graphicImage> * with support for referencing an {@link InputStream} or byte[] property in the value * attribute, optionally as a data URI. * *

Data URI

*

* Set dataURI attribute to true in order to render image in * data URI format. *

 * <o:graphicImage name="icon.png" dataURI="true" /> <!-- Faces resource as data URI -->
 * <o:graphicImage value="#{bean.icon}" dataURI="true" /> <!-- byte[]/InputStream property as data URI -->
 * 
*

* This basically renders the image inline in HTML output immediately during Faces render response phase. This approach * is very useful for a "preview" feature of uploaded images and works also in combination with view scoped beans. This * approach is however not recommended for "permanent" and/or "large" images as it doesn't offer the browser * any opportunity to cache the images for reuse, ~10KB would typically be the max even less so if there are more such * images on the same page. * *

Image streaming

*

* When not rendered as data URI, the {@link InputStream} or byte[] property must point to * a stateless @{@link GraphicImageBean} or @Named @ApplicationScoped bean. * The property will namely be evaluated at the moment the browser * requests the image content based on the URL as specified in HTML <img src>, which is usually a * different request than the one which rendered the Faces page. E.g. *

 * @Named
 * @RequestScoped
 * public class Bean {
 *
 *     private List<Image> images; // Image class should NOT have "content" property, or at least it be lazy loaded.
 *
 *     @Inject
 *     private ImageService service;
 *
 *     @PostConstruct
 *     public void init() {
 *         images = service.list();
 *     }
 *
 *     public List<Image> getImages() {
 *         return images;
 *     }
 *
 * }
 * 
*
 * @GraphicImageBean
 * public class Images {
 *
 *     @Inject
 *     private ImageService service;
 *
 *     public byte[] get(Long id) {
 *         return service.getContent(id);
 *     }
 *
 * }
 * 
*
 * <ui:repeat value="#{bean.images}" var="image">
 *     <o:graphicImage value="#{images.get(image.id)}" />
 * </ui:repeat>
 * 
*

* A @RequestScoped and @SessionScoped bean would theoretically work, but this is wrong design * (a servlet is inherently also application scoped and stateless, not without reason). A @ViewScoped * wouldn't work because the image request doesn't share the Faces view state. *

* In case the property is a method expression taking arguments, each of those arguments will be converted to a string * HTTP request parameter and back to actual objects using the converters registered by class as available via * {@link Application#createConverter(Class)}. So, most of standard types like {@link Long} are already implicitly * supported. In case you need to supply a custom object as argument for some reason, you need to explicitly register * a converter for it yourself via @FacesConverter(forClass). * *

Caching

*

* In case your "image" entity supports it, you can also supply the "last modified" property which will be used in the * ETag and Last-Modified headers and in If-Modified-Since checks, hereby * improving browser caching. The lastModified attribute supports both {@link Date} and {@link Long} as * timestamp in milliseconds. *

 * <ui:repeat value="#{bean.images}" var="image">
 *     <o:graphicImage value="#{images.get(image.id)}" lastModified="#{image.lastModified}" />
 * </ui:repeat>
 * 
*

* When unspecified, then the "default resource maximum age" as set in either the Mojarra specific context parameter * com.sun.faces.defaultResourceMaxAge or MyFaces specific context parameter * org.apache.myfaces.RESOURCE_MAX_TIME_EXPIRES will be used, else a default of 1 week will be assumed. * *

Image types

*

* When rendered as data URI, the content type will be guessed based on content header. So far, JPEG, PNG, GIF, ICO, * SVG, BMP and TIFF are recognized. If the content header is unrecognized, or when the image is rendered as regular * image source, then the content type will default to "image" without any subtype. This should work for * most images in most browsers. This may however fail on newer images or in older browsers. In that case, you can * explicitly specify the image type via the type attribute which must represent a valid file extension. * E.g. *

 * <o:graphicImage value="#{images.get(image.id)}" type="svg" />
 * 
*

* The content type will be resolved via {@link Faces#getMimeType(String)}. You can add unrecognized ones as * <mime-mapping> in web.xml. E.g. *

 * <mime-mapping>
 *     <extension>svg</extension>
 *     <mime-type>image/svg+xml</mime-type>
 * </mime-mapping>
 * 
* *

SVG view modes

*

* When serving a SVG image, you can use fragment attribute to trigger * SVG view modes * (beware of browser support). * E.g. *

 * <o:graphicImage value="#{images.get(image.id)}" type="svg" fragment="svgView(viewBox(0,50,200,200))" />
 * 
* *

Lazy loading

*

* Since OmniFaces 3.10, you can set the lazy attribute to true to indicate that the * referenced image should only be loaded when the window is finished loading and the image is visible in the viewport. *

 * <o:graphicImage ... lazy="true" />
 * 
*

* This attribute is ignored when the dataURI attribute is set to true. * *

Design notes

*

* The bean class name and method name will end up in the image source URL. Although this is technically harmless and * not tamperable by hackers, you might want to choose a "sensible" class and method name for this purpose. *

* Like <h:graphicImage>, the value attribute is ignored * when the name attribute is specified (for Faces resources). And, the value attribute of * <o:graphicImage> does not support URLs anymore. For that, just keep using * <h:graphicImage> or even plain <img>. * * @author Bauke Scholtz * @since 2.0 * @see GraphicImageBean * @see GraphicResource * @see DynamicResource * @see GraphicResourceHandler * @see DefaultResourceHandler * @see ExpressionInspector * @see MethodReference */ @FacesComponent(GraphicImage.COMPONENT_TYPE) @ResourceDependency(library=OMNIFACES_LIBRARY_NAME, name=OMNIFACES_SCRIPT_NAME, target="head") // Specifically graphicimage.js. public class GraphicImage extends HtmlGraphicImage { // Constants ------------------------------------------------------------------------------------------------------ /** The component type, which is {@value org.omnifaces.component.output.GraphicImage#COMPONENT_TYPE}. */ public static final String COMPONENT_TYPE = "org.omnifaces.component.output.GraphicImage"; /** Attribute names inherited from superclass. */ protected static final Map ATTRIBUTE_NAMES = collectAttributeNames(); private static final String ERROR_MISSING_VALUE = "o:graphicImage 'value' attribute is required."; private enum PropertyKeys { // Cannot be uppercased. They have to exactly match the attribute names. dataURI, lazy; } // Variables ------------------------------------------------------------------------------------------------------ private final State state = new State(getStateHelper()); // Constructors --------------------------------------------------------------------------------------------------- /** * Constructs the GraphicImage component. */ public GraphicImage() { setRendererType(null); } // Actions -------------------------------------------------------------------------------------------------------- @Override public void encodeBegin(FacesContext context) throws IOException { ResponseWriter writer = context.getResponseWriter(); writer.startElement("img", this); writeIdAttributeIfNecessary(writer, this); String src = getSrc(context); if (isLazy() && !isDataURI()) { writer.writeAttribute("src", "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg'/%3E", null); writer.writeAttribute("data-src", src, "value"); writer.writeAttribute("data-lazy", "true", "lazy"); } else { writer.writeAttribute("src", src, "value"); // h:graphicImage uses writeURIAttribute(), but it kills URL fragment identifiers, so we use writeAttribute() instead. } writeAttributes(writer, this, GraphicImage.ATTRIBUTE_NAMES); } @Override public void encodeEnd(FacesContext context) throws IOException { ResponseWriter writer = context.getResponseWriter(); writer.endElement("img"); } /** * Returns the URL needed for the 'src' attribute. * @param context The involved faces context. * @return The URL needed for the 'src' attribute. * @throws IOException When something fails at I/O level. */ protected String getSrc(FacesContext context) throws IOException { String name = (String) getAttributes().get("name"); boolean dataURI = isDataURI(); Resource resource; if (name != null) { resource = createGraphicResourceByName(context, name, dataURI); if (resource == null) { return RES_NOT_FOUND; } } else { ValueExpression value = getValueExpression(VALUE_ATTRIBUTE); if (value != null) { resource = createGraphicResourceByValue(context, value, dataURI); } else { throw new IllegalArgumentException(ERROR_MISSING_VALUE); } } String url = context.getExternalContext().encodeResourceURL(resource.getRequestPath()); String fragment = (String) getAttributes().get("fragment"); if (dataURI || isEmpty(fragment)) { return url; } return url + (fragment.charAt(0) == '#' ? "" : "#") + fragment; } private Resource createGraphicResourceByName(FacesContext context, String name, boolean dataURI) throws IOException { String library = (String) getAttributes().get("library"); Resource resource = createResource(context, library, name); if (resource != null && dataURI && resource.getContentType().startsWith("image")) { resource = new GraphicResource(resource.getInputStream(), resource.getContentType()); } return resource; } private Resource createGraphicResourceByValue(FacesContext context, ValueExpression value, boolean dataURI) { String type = (String) getAttributes().get("type"); if (dataURI) { return new GraphicResource(value.getValue(context.getELContext()), type); } else { return GraphicResource.create(context, value, type, getAttributes().get("lastModified")); } } // Attribute getters/setters -------------------------------------------------------------------------------------- /** * Returns an empty string as default value instead of null, so that the attribute is always rendered, * as mandated by HTML5. */ @Override public String getAlt() { return coalesce(super.getAlt(), ""); } /** * Returns whether or not to render image in data URI format. * @return Whether or not to render image in data URI format. * @since 3.10 */ public boolean isDataURI() { return state.get(PropertyKeys.dataURI, false); } /** * Sets whether or not to render image in data URI format. * @param dataURI Whether or not to render image in data URI format. * @since 3.10.1 */ public void setDataURI(boolean dataURI) { state.put(PropertyKeys.dataURI, dataURI); } /** * Returns whether or not to lazily load image. * @return Whether or not to lazily load image. * @since 3.10 */ public boolean isLazy() { return state.get(PropertyKeys.lazy, false); } /** * Sets whether or not to lazily load image. * @param lazy Whether or not to lazily load image. * @since 3.10 */ public void setLazy(boolean lazy) { state.put(PropertyKeys.lazy, lazy); } // Helpers -------------------------------------------------------------------------------------------------------- private static Map collectAttributeNames() { Map attributeNames = new HashMap<>(); for (HtmlGraphicImage.PropertyKeys propertyKey : HtmlGraphicImage.PropertyKeys.values()) { String name = propertyKey.name(); attributeNames.put(name, "styleClass".equals(name) ? "class" : propertyKey.toString()); } return Collections.unmodifiableMap(attributeNames); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy