Skip to content

Commit

Permalink
Fix #289: add @GraphicImageScoped
Browse files Browse the repository at this point in the history
  • Loading branch information
Bauke Scholtz committed Jul 23, 2016
1 parent 8ab2772 commit 15ed981
Show file tree
Hide file tree
Showing 4 changed files with 150 additions and 51 deletions.
4 changes: 4 additions & 0 deletions src/main/java/org/omnifaces/ApplicationListener.java
Expand Up @@ -23,13 +23,15 @@
import javax.servlet.annotation.WebListener;

import org.omnifaces.cdi.Eager;
import org.omnifaces.cdi.GraphicImageScoped;
import org.omnifaces.cdi.eager.EagerBeansRepository;
import org.omnifaces.cdi.eager.EagerBeansWebListener;
import org.omnifaces.cdi.push.Socket;
import org.omnifaces.component.output.Cache;
import org.omnifaces.component.output.cache.CacheInitializer;
import org.omnifaces.eventlistener.DefaultServletContextListener;
import org.omnifaces.facesviews.FacesViews;
import org.omnifaces.resourcehandler.GraphicResource;

/**
* <p>
Expand All @@ -41,6 +43,7 @@
* <li>Add {@link FacesViews} mappings to FacesServlet if necessary.
* <li>Load {@link Cache} provider and register its filter if necessary.
* <li>Register {@link Socket} endpoint if necessary.
* <li>Register {@link GraphicImageScoped} beans in {@link GraphicResource}.
* </ol>
*
* @author Bauke Scholtz
Expand All @@ -64,6 +67,7 @@ public void contextInitialized(ServletContextEvent event) {
FacesViews.addMappings(servletContext);
CacheInitializer.loadProviderAndRegisterFilter(servletContext);
Socket.registerEndpointIfNecessary(servletContext);
GraphicResource.registerGraphicImageScopedBeans();
}
catch (Throwable e) {
logger.log(SEVERE, ERROR_OMNIFACES_INITIALIZATION_FAIL, e);
Expand Down
71 changes: 71 additions & 0 deletions src/main/java/org/omnifaces/cdi/GraphicImageScoped.java
@@ -0,0 +1,71 @@
/*
* Copyright 2016 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.cdi;

import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.inject.Stereotype;
import javax.inject.Qualifier;

import org.omnifaces.component.output.GraphicImage;

/**
* <p>
* Stereo type that designates a bean as an application scoped bean for serving graphic images via
* <code>&lt;o:graphicImage&gt;</code> component or <code>#{of:graphicImageURL()}</code> EL functions.
* <pre>
* import javax.inject.Named;
* import org.omnifaces.cdi.GraphicImageScoped;
*
* &#64;Named
* &#64;GraphicImageScoped
* public class Images {
*
* &#64;Inject
* private ImageService service;
*
* public byte[] get(Long id) {
* return service.getContent(id);
* }
*
* }
* </pre>
* <p>
* When using {@link ApplicationScoped} instead, serving graphic images via a JSF page will continue to work, but
* when the server restarts, then hotlinking/bookmarking will stop working until the JSF page referencing the same
* bean method is requested for the first time. The {@link GraphicImageScoped} basically enables serving images without
* the need to reference them via a JSF page.
*
* @since 2.5
* @author Bauke Scholtz
* @see GraphicImage
*
*/
@Documented
@Qualifier
@Stereotype
@ApplicationScoped
@Retention(RUNTIME)
@Target(TYPE)
public @interface GraphicImageScoped {
//
}
24 changes: 13 additions & 11 deletions src/main/java/org/omnifaces/component/output/GraphicImage.java
Expand Up @@ -33,6 +33,7 @@
import javax.faces.context.FacesContext;
import javax.faces.context.ResponseWriter;

import org.omnifaces.cdi.GraphicImageScoped;
import org.omnifaces.el.ExpressionInspector;
import org.omnifaces.el.MethodReference;
import org.omnifaces.resourcehandler.DefaultResourceHandler;
Expand Down Expand Up @@ -65,10 +66,10 @@
* <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>@ApplicationScoped</code> bean (both JSF and CDI 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>&lt;img src&gt;</code>, which is usually a different request than the one which rendered the JSF page.
* E.g.
* a <em>stateless</em> <code>@GraphicImageScoped</code> or <code>@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>&lt;img src&gt;</code>, which is usually a different request than
* the one which rendered the JSF page. E.g.
* <pre>
* &#64;Named
* &#64;RequestScoped
Expand All @@ -92,21 +93,21 @@
* </pre>
* <pre>
* &#64;Named
* &#64;ApplicationScoped
* public class ImageStreamer {
* &#64;GraphicImageScoped
* public class Images {
*
* &#64;Inject
* private ImageService service;
*
* public byte[] getById(Long id) {
* public byte[] get(Long id) {
* return service.getContent(id);
* }
*
* }
* </pre>
* <pre>
* &lt;ui:repeat value="#{bean.images}" var="image"&gt;
* &lt;o:graphicImage value="#{imageStreamer.getById(image.id)}" /&gt;
* &lt;o:graphicImage value="#{images.get(image.id)}" /&gt;
* &lt;/ui:repeat&gt;
* </pre>
* <p>
Expand All @@ -128,7 +129,7 @@
* timestamp in milliseconds.
* <pre>
* &lt;ui:repeat value="#{bean.images}" var="image"&gt;
* &lt;o:graphicImage value="#{imageStreamer.getById(image.id)}" lastModified="#{image.lastModified}" /&gt;
* &lt;o:graphicImage value="#{images.get(image.id)}" lastModified="#{image.lastModified}" /&gt;
* &lt;/ui:repeat&gt;
* </pre>
*
Expand All @@ -141,7 +142,7 @@
* explicitly specify the image type via the <code>type</code> attribute which must represent a valid file extension.
* E.g.
* <pre>
* &lt;o:graphicImage value="#{imageStreamer.getById(image.id)}" type="svg" /&gt;
* &lt;o:graphicImage value="#{images.get(image.id)}" type="svg" /&gt;
* </pre>
* <p>
* The content type will be resolved via {@link Faces#getMimeType(String)}. You can add unrecognized ones as
Expand All @@ -160,7 +161,7 @@
* (beware of <a href="http://caniuse.com/#feat=svg-fragment">browser support</a>).
* E.g.
* <pre>
* &lt;o:graphicImage value="#{imageStreamer.getById(image.id)}" type="svg" fragment="svgView(viewBox(0,50,200,200))" /&gt;
* &lt;o:graphicImage value="#{images.get(image.id)}" type="svg" fragment="svgView(viewBox(0,50,200,200))" /&gt;
* </pre>
*
* <h3>Design notes</h3>
Expand All @@ -175,6 +176,7 @@
*
* @author Bauke Scholtz
* @since 2.0
* @see GraphicImageScoped
* @see GraphicResource
* @see DynamicResource
* @see GraphicResourceHandler
Expand Down
102 changes: 62 additions & 40 deletions src/main/java/org/omnifaces/resourcehandler/GraphicResource.java
Expand Up @@ -20,6 +20,7 @@
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.isOneOf;
import static org.omnifaces.util.Utils.toByteArray;

import java.io.ByteArrayInputStream;
Expand All @@ -36,6 +37,8 @@
import java.util.concurrent.ConcurrentHashMap;

import javax.el.ValueExpression;
import javax.enterprise.inject.spi.CDI;
import javax.enterprise.util.AnnotationLiteral;
import javax.faces.FacesException;
import javax.faces.application.Application;
import javax.faces.application.Resource;
Expand All @@ -45,6 +48,7 @@
import javax.faces.convert.Converter;
import javax.xml.bind.DatatypeConverter;

import org.omnifaces.cdi.GraphicImageScoped;
import org.omnifaces.el.ExpressionInspector;
import org.omnifaces.el.MethodReference;
import org.omnifaces.util.Faces;
Expand All @@ -64,14 +68,25 @@ public class GraphicResource extends DynamicResource {
private static final Map<String, String> CONTENT_TYPES_BY_BASE64_HEADER = createContentTypesByBase64Header();
private static final Map<String, MethodReference> ALLOWED_METHODS = new ConcurrentHashMap<>();
private static final String[] EMPTY_PARAMS = new String[0];
private static final int RESOURCE_NAME_FULL_PARTS_LENGTH = 3;

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

private static final String ERROR_INVALID_LASTMODIFIED =
@SuppressWarnings("unchecked")
private static final Class<? extends Annotation>[] REQUIRED_RETURN_TYPES = new Class[] {
InputStream.class,
byte[].class
};

private static final AnnotationLiteral<GraphicImageScoped> GRAPHIC_IMAGE_SCOPED = new AnnotationLiteral<GraphicImageScoped>() {
private static final long serialVersionUID = 1L;
};

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_INVALID_TYPE =
Expand Down Expand Up @@ -126,7 +141,7 @@ public GraphicResource(Object content, String contentType) {
setContentType(guessContentType(base64));
}
else if (!contentType.contains("/")) {
setContentType(resolveContentType(contentType));
setContentType(getContentType("image." + contentType));
}
}

Expand Down Expand Up @@ -163,7 +178,7 @@ else if (lastModified != null) {
* Create a new graphic resource based on the given value expression.
* @param context The involved faces context.
* @param value The value expression representing content to create a new graphic resource for.
* @param type The image type, represented as file extension. E.g. "jpg", "png", "gif", "ico", "svg", "bmp",
* @param type The image type, represented as file extension. E.g. "jpg", "png", "gif", "ico", "svg", "bmp",
* "tiff", etc.
* @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}.
Expand All @@ -175,26 +190,30 @@ else if (lastModified != null) {
*/
public static GraphicResource create(FacesContext context, ValueExpression value, String type, Object lastModified) {
MethodReference methodReference = ExpressionInspector.getMethodReference(context.getELContext(), value);
Method beanMethod = methodReference.getMethod();

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

String name = getResourceName(methodReference, type);
Class<?> beanClass = methodReference.getBase().getClass();
String name = getResourceBaseName(beanClass, beanMethod);

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()));
if (!isOneOf(beanMethod.getReturnType(), REQUIRED_RETURN_TYPES)) {
throw new IllegalArgumentException(String.format(ERROR_INVALID_RETURNTYPE, beanMethod.getReturnType()));
}

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

Object[] params = methodReference.getActualParameters();
String[] convertedParams = convertToStrings(context, params, methodReference.getMethod().getParameterTypes());
return new GraphicResource(name, convertedParams, lastModified);
String[] convertedParams = convertToStrings(context, params, beanMethod.getParameterTypes());
return new GraphicResource(name + (isEmpty(type) ? "" : "." + type), convertedParams, lastModified);
}

/**
Expand Down Expand Up @@ -235,39 +254,56 @@ public InputStream getInputStream() throws IOException {
throw new FacesException(e);
}

if (content == null) {
return null;
}
else if (content instanceof InputStream) {
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));
return null;
}
}

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

/**
* This must return an unique and URL-safe identifier of the bean+method+type without any periods.
* Register graphic image scoped beans discovered so far.
*/
public static void registerGraphicImageScopedBeans() {
for (Object bean : CDI.current().select(GRAPHIC_IMAGE_SCOPED)) {
for (Method method : bean.getClass().getMethods()) {
if (isOneOf(method.getReturnType(), REQUIRED_RETURN_TYPES)) {
String resourceBaseName = getResourceBaseName(bean.getClass().getSuperclass(), method);
MethodReference methodReference = new MethodReference(bean, method);
ALLOWED_METHODS.put(resourceBaseName, methodReference);
}
}
}
}

/**
* This must return an unique and URL-safe identifier of the bean+method without any periods.
*/
private static String getResourceName(MethodReference methodReference, String type) {
return methodReference.getBase().getClass().getSimpleName().replaceAll("\\W", "")
+ "_" + methodReference.getMethod().getName()
+ (isEmpty(type) ? "" : ("_" + type));
private static String getResourceBaseName(Class<?> beanClass, Method beanMethod) {
return beanClass.getSimpleName().replaceAll("\\W", "") + "_" + beanMethod.getName();
}

/**
* This must extract the content type from the resource name, if any, else return the default content type.
*/
private static String getContentType(String resourceName) {
String[] parts = resourceName.split("_");
return (parts.length == RESOURCE_NAME_FULL_PARTS_LENGTH)
? resolveContentType(parts[RESOURCE_NAME_FULL_PARTS_LENGTH - 1])
: DEFAULT_CONTENT_TYPE;
if (!resourceName.contains(".")) {
return DEFAULT_CONTENT_TYPE;
}

String contentType = Faces.getExternalContext().getMimeType(resourceName);

if (contentType == null) {
throw new IllegalArgumentException(String.format(ERROR_INVALID_TYPE, resourceName.split("\\.", 2)[1]));
}

return contentType;
}

/**
Expand All @@ -283,20 +319,6 @@ private static String guessContentType(String base64) {
return DEFAULT_CONTENT_TYPE;
}

/**
* Resolve image content type based on given type attribute.
* @throws IllegalArgumentException When given type is unrecognized.
*/
private static String resolveContentType(String type) {
String contentType = Faces.getExternalContext().getMimeType("image." + type);

if (contentType == null) {
throw new IllegalArgumentException(String.format(ERROR_INVALID_TYPE, type));
}

return contentType;
}

/**
* Convert the given resource content to base64 encoded string.
* @throws IllegalArgumentException When given content is unrecognized.
Expand Down

0 comments on commit 15ed981

Please sign in to comment.