org.omnifaces.component.output.GraphicImage Maven / Gradle / Ivy
/*
* 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);
}
}