-
Notifications
You must be signed in to change notification settings - Fork 96
/
GraphicResource.java
396 lines (343 loc) · 15.5 KB
/
GraphicResource.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
/*
* Copyright 2014 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.resourcehandler;
import static java.lang.reflect.Modifier.isPublic;
import static java.util.Arrays.asList;
import static java.util.Collections.singletonMap;
import static org.omnifaces.util.Faces.getContext;
import static org.omnifaces.util.Servlets.toQueryString;
import static org.omnifaces.util.Utils.coalesce;
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;
import java.io.IOException;
import java.io.InputStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
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;
import javax.faces.component.UIComponent;
import javax.faces.component.UIOutput;
import javax.faces.context.FacesContext;
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;
/**
* <p>
* This {@link Resource} implementation is used by the {@link org.omnifaces.component.output.GraphicImage} component.
*
* @author Bauke Scholtz
* @since 2.0
*/
public class GraphicResource extends DynamicResource {
// Constants ------------------------------------------------------------------------------------------------------
private static final String DEFAULT_CONTENT_TYPE = "image";
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];
@SuppressWarnings("unchecked")
private static final Class<? extends Annotation>[] REQUIRED_ANNOTATION_TYPES = new Class[] {
GraphicImageScoped.class,
javax.faces.bean.ApplicationScoped.class,
javax.enterprise.context.ApplicationScoped.class
};
@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 =
"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'.";
private static final String ERROR_INVALID_SCOPE =
"o:graphicImage 'value' attribute must refer an @ApplicationScoped bean."
+ " Cannot find the right annotation on bean class '%s'.";
private static final String ERROR_INVALID_RETURNTYPE =
"o:graphicImage 'value' attribute must represent a method returning an InputStream or byte[]."
+ " Encountered an invalid return value of '%s'.";
private static final String ERROR_INVALID_PARAMS =
"o:graphicImage 'value' attribute must specify valid method parameters."
+ " Encountered invalid method parameters '%s'.";
private static Map<String, String> createContentTypesByBase64Header() {
Map<String, String> contentTypesByBase64Header = new HashMap<>();
contentTypesByBase64Header.put("/9j/", "image/jpeg");
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");
return Collections.unmodifiableMap(contentTypesByBase64Header);
}
// Variables ------------------------------------------------------------------------------------------------------
private String base64;
private String[] params;
// Constructors ---------------------------------------------------------------------------------------------------
/**
* Construct a new graphic resource which uses the given content as data URI.
* This constructor is called during render time of <code><o:graphicImage ... dataURI="true"></code>.
* @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, 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);
base64 = convertToBase64(content);
if (contentType == null) {
setContentType(guessContentType(base64));
}
else if (!contentType.contains("/")) {
setContentType(getContentType("image." + contentType));
}
}
/**
* Construct a new graphic resource based on the given name, EL method parameters converted as string, and the
* "last modified" representation.
* This constructor is called during render time of <code><o:graphicImage value="..." dataURI="false"></code>
* and during handling the resource request by {@link GraphicResourceHandler}.
* @param name The graphic resource name, usually representing the base and method of EL method expression.
* @param params The graphic resource method parameters.
* @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}.
* @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, getContentType(name));
this.params = coalesce(params, EMPTY_PARAMS);
if (lastModified instanceof Long) {
setLastModified((Long) lastModified);
}
else if (lastModified instanceof Date) {
setLastModified(((Date) lastModified).getTime());
}
else if (isNumber(String.valueOf(lastModified))) {
setLastModified(Long.valueOf(lastModified.toString()));
}
else if (lastModified != null) {
throw new IllegalArgumentException(String.format(ERROR_INVALID_LASTMODIFIED, lastModified));
}
}
/**
* 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",
* "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. Or, when the "type"
* attribute does not represent a valid file extension (you can add unrecognized ones as
* <code><mime-mapping></code> in <code>web.xml</code>).
*/
public static GraphicResource create(FacesContext context, ValueExpression value, String type, Object lastModified) {
MethodReference methodReference = ExpressionInspector.getMethodReference(context.getELContext(), value);
Method beanMethod = methodReference.getMethod();
if (beanMethod == null) {
throw new IllegalArgumentException(String.format(ERROR_UNKNOWN_METHOD, value.getExpressionString()));
}
Class<?> beanClass = methodReference.getBase().getClass();
String name = getResourceBaseName(beanClass, beanMethod);
if (!ALLOWED_METHODS.containsKey(name)) { // No need to validate everytime when already known.
if (!isOneAnnotationPresent(beanClass, REQUIRED_ANNOTATION_TYPES)) {
throw new IllegalArgumentException(String.format(ERROR_INVALID_SCOPE, beanClass));
}
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, beanMethod.getParameterTypes());
return new GraphicResource(name + (isEmpty(type) ? "" : "." + type), convertedParams, lastModified);
}
/**
* An override which either returns the data URI or appends the converted method parameters to the query string.
*/
@Override
public String getRequestPath() {
if (base64 != null) {
return "data:" + getContentType() + ";base64," + base64;
}
else {
String queryString = isEmpty(params) ? "" : ("&" + toQueryString(singletonMap("p", asList(params))));
return super.getRequestPath() + queryString;
}
}
@Override
public InputStream getInputStream() throws IOException {
MethodReference methodReference = ALLOWED_METHODS.get(getResourceName().split("\\.", 2)[0]);
Method method;
Object[] convertedParams;
try {
method = methodReference.getMethod();
convertedParams = convertToObjects(getContext(), params, method.getParameterTypes());
}
catch (Exception e) {
return null; // Ignore hacker attempts. I'd rather return 400 here, but JSF spec doesn't support it.
}
Object content;
try {
content = method.invoke(methodReference.getBase(), convertedParams);
}
catch (Exception e) {
throw new FacesException(e);
}
if (content instanceof InputStream) {
return (InputStream) content;
}
else if (content instanceof byte[]) {
return new ByteArrayInputStream((byte[]) content);
}
else {
return null;
}
}
// Helpers --------------------------------------------------------------------------------------------------------
/**
* 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 (isPublic(method.getModifiers()) && 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 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) {
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;
}
/**
* Guess the image content type based on given base64 encoded content for data URI.
*/
private static String guessContentType(String base64) {
for (Entry<String, String> contentTypeByBase64Header : CONTENT_TYPES_BY_BASE64_HEADER.entrySet()) {
if (base64.startsWith(contentTypeByBase64Header.getKey())) {
return contentTypeByBase64Header.getValue();
}
}
return DEFAULT_CONTENT_TYPE;
}
/**
* 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.
*/
private static String[] convertToStrings(FacesContext context, Object[] values, Class<?>[] types) {
validateParamLength(values, types);
String[] strings = new String[values.length];
Application application = context.getApplication();
UIComponent dummyComponent = new UIOutput();
for (int i = 0; i < values.length; i++) {
Object value = values[i];
Converter converter = application.createConverter(types[i]);
strings[i] = (converter != null)
? converter.getAsString(context, dummyComponent, value)
: (value != null) ? value.toString() : "";
}
return strings;
}
/**
* Convert the given strings to objects using converters registered on given types.
* @throws IllegalArgumentException When the length of given params doesn't match those of given types.
*/
private static Object[] convertToObjects(FacesContext context, String[] values, Class<?>[] types) {
validateParamLength(values, types);
Object[] objects = new Object[values.length];
Application application = context.getApplication();
UIComponent dummyComponent = new UIOutput();
for (int i = 0; i < values.length; i++) {
String value = isEmpty(values[i]) ? null : values[i];
Converter converter = application.createConverter(types[i]);
objects[i] = (converter != null)
? converter.getAsObject(context, dummyComponent, value)
: value;
}
return objects;
}
private static void validateParamLength(Object[] params, Class<?>[] types) {
if (params.length != types.length) {
throw new IllegalArgumentException(String.format(ERROR_INVALID_PARAMS, Arrays.toString(params)));
}
}
}