Permalink
Browse files

Recognize SVG image in data URI and add new type attribute to explicitly

specify content type via mime mapping.
  • Loading branch information...
BalusC committed Feb 5, 2015
1 parent 9ed81f2 commit 070e7b53fb551c1d9df457d9d14a6a460b654995
@@ -35,6 +35,7 @@
import org.omnifaces.resourcehandler.DynamicResource;
import org.omnifaces.resourcehandler.GraphicResource;
import org.omnifaces.resourcehandler.GraphicResourceHandler;
import org.omnifaces.util.Faces;
/**
* <p>
@@ -127,6 +128,27 @@
* &lt;/ui:repeat&gt;
* </pre>
*
* <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>
* &lt;o:graphicImage value="#{imageStreamer.getById(image.id)}" type="svg" /&gt;
* </pre>
* <p>
* The content type will be resolved via {@link Faces#getMimeType(String)}. You can add unrecognized ones as
* <code>&lt;mime-mapping&gt;</code> in <code>web.xml</code>. E.g.
* <pre>
* &lt;mime-mapping&gt;
* &lt;extension&gt;svg&lt;/extension&gt;
* &lt;mime-type&gt;image/svg+xml&lt;/mime-type&gt;
* &lt;/mime-mapping&gt;
* </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
@@ -220,11 +242,13 @@ protected String getSrc(FacesContext context) throws IOException {
throw new IllegalArgumentException(ERROR_MISSING_VALUE);
}
if (dataURI) {
resource = new GraphicResource(value.getValue(context.getELContext()), null);
String type = (String) getAttributes().get("type");
if (dataURI) {
resource = new GraphicResource(value.getValue(context.getELContext()), type);
}
else {
resource = GraphicResource.create(context, value, getAttributes().get("lastModified"));
resource = GraphicResource.create(context, value, type, getAttributes().get("lastModified"));
}
}
@@ -46,6 +46,7 @@
import org.omnifaces.component.output.GraphicImage;
import org.omnifaces.el.ExpressionInspector;
import org.omnifaces.el.MethodReference;
import org.omnifaces.util.Faces;
/**
* <p>
@@ -69,9 +70,12 @@
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_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 =
"o:graphicImage 'type' attribute must represent a valid file extension."
+ " 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'.";
@@ -91,6 +95,7 @@
contentTypesByBase64Header.put("iVBORw", "image/png");
contentTypesByBase64Header.put("R0lGOD", "image/gif");
contentTypesByBase64Header.put("AAABAA", "image/x-icon");
contentTypesByBase64Header.put("PD94bW", "image/svg+xml");
contentTypesByBase64Header.put("Qk0", "image/bmp");
contentTypesByBase64Header.put("SUkqAA", "image/tiff");
contentTypesByBase64Header.put("TU0AKg", "image/tiff");
@@ -99,7 +104,7 @@
// Variables ------------------------------------------------------------------------------------------------------
private Object content;
private String base64;
private String[] params;
// Constructors ---------------------------------------------------------------------------------------------------
@@ -108,11 +113,19 @@
* 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.
* based on the content type signature in the content header. So far, JPEG, PNG, GIF, ICO, SVG, BMP and TIFF are
* recognized. Else if this represents the file extension, then it will be resolved based on mime mappings.
*/
public GraphicResource(Object content, String contentType) {
super("", GraphicResourceHandler.LIBRARY_NAME, contentType);
this.content = content;
base64 = convertToBase64(content);
if (contentType == null) {
setContentType(guessContentType(base64));
}
else if (!contentType.contains("/")) {
setContentType(resolveContentType(contentType));
}
}
/**
@@ -125,7 +138,7 @@ public GraphicResource(Object content, String contentType) {
* @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);
super(name, GraphicResourceHandler.LIBRARY_NAME, getContentType(name));
this.params = coalesce(params, EMPTY_PARAMS);
if (lastModified instanceof Long) {
@@ -147,20 +160,24 @@ else if (lastModified != null) {
* 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 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}.
* @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.
* represent a method expression referring an existing method taking at least one argument. Or, when the "type"
* attribute does not represent a valid file extension (you can add unrecognized ones as
* <code>&lt;mime-mapping&gt;</code> in <code>web.xml</code>).
*/
public static GraphicResource create(FacesContext context, ValueExpression value, Object lastModified) {
public static GraphicResource create(FacesContext context, ValueExpression value, String type, 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);
String name = getResourceName(methodReference, type);
if (!ALLOWED_METHODS.containsKey(name)) { // No need to validate everytime when already known.
Class<? extends Object> beanClass = methodReference.getBase().getClass();
@@ -182,47 +199,15 @@ public static GraphicResource create(FacesContext context, ValueExpression value
*/
@Override
public String getRequestPath() {
if (content != null) {
return getDataURI();
if (base64 != null) {
return "data:" + getContentType() + ";base64," + base64;
}
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]);
@@ -233,6 +218,7 @@ public InputStream getInputStream() throws IOException {
Method method = methodReference.getMethod();
Object[] convertedParams = convertToObjects(getContext(), params, method.getParameterTypes());
Object content;
try {
content = method.invoke(methodReference.getBase(), convertedParams);
@@ -241,7 +227,10 @@ public InputStream getInputStream() throws IOException {
throw new FacesException(e);
}
if (content instanceof InputStream) {
if (content == null) {
return null;
}
else if (content instanceof InputStream) {
return (InputStream) content;
}
else if (content instanceof byte[]) {
@@ -254,12 +243,21 @@ else if (content instanceof byte[]) {
// 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();
}
/**
* This must return an unique and URL-safe identifier of the bean+method+type without any periods.
*/
private static String getResourceName(MethodReference methodReference, String type) {
return methodReference.getBase().getClass().getSimpleName() + "_" + methodReference.getMethod().getName()
+ (isEmpty(type) ? "" : ("_" + type));
}
/**
* 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 == 3) ? resolveContentType(parts[2]) : DEFAULT_CONTENT_TYPE;
}
/**
* Guess the image content type based on given base64 encoded content for data URI.
@@ -274,6 +272,45 @@ 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.
*/
private static String convertToBase64(Object content) {
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));
}
return DatatypeConverter.printBase64Binary(bytes);
}
/**
* 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.
@@ -2727,6 +2727,17 @@ public class ValidateValuesBean implements MultiFieldValidator {
<required>false</required>
<type>boolean</type>
</attribute>
<attribute>
<description>
<![CDATA[
The image type, represented as file extension. E.g. "jpg", "png", "gif", "ico", "svg", "bmp", "tiff", etc.
This attribute is ignored when 'name' attribute is specified.
]]>
</description>
<name>type</name>
<required>false</required>
<type>java.lang.String</type>
</attribute>
<attribute>
<description>
<![CDATA[
@@ -2743,7 +2754,7 @@ public class ValidateValuesBean implements MultiFieldValidator {
<description>
<![CDATA[
The resource name of the resource. Works the same way as on <code>&lt;h:graphicImage&gt;</code>.
When this attribute is specified, 'value' and 'lastModified' attributes are ignored.
When this attribute is specified, 'value', 'type' and 'lastModified' attributes are ignored.
]]>
</description>
<name>name</name>

0 comments on commit 070e7b5

Please sign in to comment.