/*
* Copyright 2017 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.component.output;
import static java.lang.Boolean.parseBoolean;
import static org.omnifaces.resourcehandler.DefaultResourceHandler.RES_NOT_FOUND;
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 javax.el.ValueExpression;
import javax.faces.application.Application;
import javax.faces.application.Resource;
import javax.faces.component.FacesComponent;
import javax.faces.component.html.HtmlGraphicImage;
import javax.faces.context.FacesContext;
import javax.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;
/**
* <p>
* The <code><o:graphicImage></code> is a component that extends the standard <code><h:graphicImage></code>
* with support for referencing an {@link InputStream} or <code>byte[]</code> property in the <code>value</code>
* attribute, optionally as a data URI.
*
* <h3>Data URI</h3>
* <p>
* Set <code>dataURI</code> attribute to true in order to render image in
* <a href="http://en.wikipedia.org/wiki/Data_URI_scheme">data URI format</a>.
* <pre>
* <o:graphicImage name="icon.png" dataURI="true" /> <!-- JSF resource as data URI -->
* <o:graphicImage value="#{bean.icon}" dataURI="true" /> <!-- byte[]/InputStream property as data URI -->
* </pre>
* <p>
* This basically renders the image inline in HTML output immediately during JSF 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 <em>not</em> 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.
*
* <h3>Image streaming</h3>
* <p>
* When not rendered as data URI, the {@link InputStream} or <code>byte[]</code> property <strong>must</strong> point to
* a <em>stateless</em> <code>@</code>{@link GraphicImageBean} or <code>@Named @ApplicationScoped</code> bean
* (both JSF and CDI application scopes are supported). The property will namely be evaluated at the moment the browser
* requests the image content based on the URL as specified in HTML <code><img src></code>, which is usually a
* different request than the one which rendered the JSF page. E.g.
* <pre>
* @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;
* }
*
* }
* </pre>
* <pre>
* @GraphicImageBean
* public class Images {
*
* @Inject
* private ImageService service;
*
* public byte[] get(Long id) {
* return service.getContent(id);
* }
*
* }
* </pre>
* <pre>
* <ui:repeat value="#{bean.images}" var="image">
* <o:graphicImage value="#{images.get(image.id)}" />
* </ui:repeat>
* </pre>
* <p>
* A <code>@RequestScoped</code> and <code>@SessionScoped</code> bean would theoretically work, but this is wrong design
* (a servlet is inherently also application scoped and stateless, not without reason). A <code>@ViewScoped</code>
* wouldn't work because the image request doesn't share the JSF view state.
* <p>
* 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 <code>@FacesConverter(forClass)</code>.
*
* <h3>Caching</h3>
* <p>
* In case your "image" entity supports it, you can also supply the "last modified" property which will be used in the
* <code>ETag</code> and <code>Last-Modified</code> headers and in <code>If-Modified-Since</code> checks, hereby
* improving browser caching. The <code>lastModified</code> attribute supports both {@link Date} and {@link Long} as
* timestamp in milliseconds.
* <pre>
* <ui:repeat value="#{bean.images}" var="image">
* <o:graphicImage value="#{images.get(image.id)}" lastModified="#{image.lastModified}" />
* </ui:repeat>
* </pre>
* <p>
* When unspecified, then the "default resource maximum age" as set in either the Mojarra specific context parameter
* <code>com.sun.faces.defaultResourceMaxAge</code> or MyFaces specific context parameter
* <code>org.apache.myfaces.RESOURCE_MAX_TIME_EXPIRES</code> will be used, else a default of 1 week will be assumed.
*
* <h3>Image types</h3>
* <p>
* 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 <code>"image"</code> 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 <code>type</code> attribute which must represent a valid file extension.
* E.g.
* <pre>
* <o:graphicImage value="#{images.get(image.id)}" type="svg" />
* </pre>
* <p>
* The content type will be resolved via {@link Faces#getMimeType(String)}. You can add unrecognized ones as
* <code><mime-mapping></code> in <code>web.xml</code>. E.g.
* <pre>
* <mime-mapping>
* <extension>svg</extension>
* <mime-type>image/svg+xml</mime-type>
* </mime-mapping>
* </pre>
*
* <h3>SVG view modes</h3>
* <p>
* When serving a SVG image, you can use <code>fragment</code> attribute to trigger
* <a href="http://www.w3.org/TR/SVG/linking.html#LinksIntoSVG">SVG view modes</a>
* (beware of <a href="http://caniuse.com/#feat=svg-fragment">browser support</a>).
* E.g.
* <pre>
* <o:graphicImage value="#{images.get(image.id)}" type="svg" fragment="svgView(viewBox(0,50,200,200))" />
* </pre>
*
* <h3>Design notes</h3>
* <p>
* 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.
* <p>
* Like <code><h:graphicImage></code>, the <code>value</code> attribute is <strong>ignored</strong>
* when the <code>name</code> attribute is specified (for JSF resources). And, the <code>value</code> attribute of
* <code><o:graphicImage></code> does <strong>not</strong> support URLs anymore. For that, just keep using
* <code><h:graphicImage></code> or even plain <code><img></code>.
*
* @author Bauke Scholtz
* @since 2.0
* @see GraphicImageBean
* @see GraphicResource
* @see DynamicResource
* @see GraphicResourceHandler
* @see DefaultResourceHandler
* @see ExpressionInspector
* @see MethodReference
*/
@FacesComponent(GraphicImage.COMPONENT_TYPE)
public class GraphicImage extends HtmlGraphicImage {
// Constants ------------------------------------------------------------------------------------------------------
public static final String COMPONENT_TYPE = "org.omnifaces.component.output.GraphicImage";
protected static final Map<String, String> ATTRIBUTE_NAMES = collectAttributeNames();
private static final String ERROR_MISSING_VALUE = "o:graphicImage 'value' attribute is required.";
// 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);
writer.writeAttribute("src", getSrc(context), "value"); // writeURIAttribute kills URL fragment identifiers.
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 = parseBoolean(String.valueOf(getAttributes().get("dataURI")));
Resource resource;
if (name != null) {
resource = createGraphicResourceByName(context, name, dataURI);
if (resource == null) {
return RES_NOT_FOUND;
}
}
else {
ValueExpression value = getValueExpression("value");
if (value != null) {
resource = createGraphicResourceByValue(context, value, dataURI);
}
else {
throw new IllegalArgumentException(ERROR_MISSING_VALUE);
}
}
String fragment = (String) getAttributes().get("fragment");
String fragmentString = dataURI || isEmpty(fragment) ? "" : ((fragment.charAt(0) == '#' ? "" : "#") + fragment);
return context.getExternalContext().encodeResourceURL(resource.getRequestPath()) + fragmentString;
}
private Resource createGraphicResourceByName(FacesContext context, String name, boolean dataURI) throws IOException {
String library = (String) getAttributes().get("library");
Resource resource = context.getApplication().getResourceHandler().createResource(name, library);
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"));
}
}
/**
* Returns an empty string as default value instead of <code>null</code>, so that the attribute is always rendered,
* as mandated by HTML5.
*/
@Override
public String getAlt() {
return coalesce(super.getAlt(), "");
}
// Helpers --------------------------------------------------------------------------------------------------------
private static Map<String, String> collectAttributeNames() {
Map<String, String> attributeNames = new HashMap<>();
for (PropertyKeys propertyKey : PropertyKeys.values()) {
String name = propertyKey.name();
attributeNames.put(name, "styleClass".equals(name) ? "class" : propertyKey.toString());
}
return Collections.unmodifiableMap(attributeNames);
}
}