1 | package com.hammurapi.render; |
2 | |
3 | import java.io.File; |
4 | import java.io.FileOutputStream; |
5 | import java.io.IOException; |
6 | import java.io.InputStream; |
7 | import java.io.StringWriter; |
8 | import java.io.Writer; |
9 | import java.lang.reflect.Constructor; |
10 | import java.util.HashMap; |
11 | import java.util.Locale; |
12 | import java.util.Map; |
13 | import java.util.WeakHashMap; |
14 | import java.util.concurrent.ConcurrentHashMap; |
15 | |
16 | import com.hammurapi.common.Context; |
17 | import com.hammurapi.common.IdentityManager; |
18 | import com.hammurapi.common.Pumper; |
19 | import com.hammurapi.convert.ConvertingService; |
20 | import com.hammurapi.convert.DuckConverterFactory; |
21 | |
22 | /** |
23 | * Helper class for ReportGenerator and Jxp renderers. |
24 | * It is available in Jxp templates in ReportGenerator |
25 | * as <code>renderHelper</code> variable. |
26 | * @author Pavel Vlasov |
27 | * |
28 | */ |
29 | public class RenderHelper implements RenderingConstants { |
30 | |
31 | /** |
32 | * Keeps object attributes. |
33 | */ |
34 | private static final Map<Object, Map<String, Object>> attributes = new WeakHashMap<Object, Map<String,Object>>(); |
35 | |
36 | // private static final int BUCKETS = 200; |
37 | private Map<String, Object> env; |
38 | private Context context; |
39 | private Locale locale; |
40 | |
41 | private IdentityManager<?> identityManager; |
42 | private File outputDir; |
43 | private Map<Class<?>, String> classImages = new ConcurrentHashMap<Class<?>, String>(); |
44 | private boolean http; |
45 | |
46 | /** |
47 | * @return Output directory. |
48 | */ |
49 | public File getOutputDir() { |
50 | return outputDir; |
51 | } |
52 | |
53 | /** |
54 | * @return Identity manager. |
55 | */ |
56 | public IdentityManager<?> getIdentityManager() { |
57 | return identityManager; |
58 | } |
59 | |
60 | /** |
61 | * Constructor. |
62 | * @param identityManager Identity manager. |
63 | * @param outputDir Output directory. |
64 | * @param env Environment. |
65 | * @param context Context. |
66 | * @param locale Locale. |
67 | */ |
68 | public RenderHelper( |
69 | IdentityManager<?> identityManager, |
70 | File outputDir, |
71 | Map<String, Object> env, |
72 | Context context, |
73 | Locale locale, |
74 | boolean http) { |
75 | super(); |
76 | this.identityManager = identityManager; |
77 | this.outputDir = outputDir; |
78 | this.env = env; |
79 | this.context = context; |
80 | this.locale = locale; |
81 | this.http = http; |
82 | } |
83 | |
84 | /** |
85 | * Finds image file for a given object in classloader, |
86 | * writes it to the "images" directory in the |
87 | * output directory, if it doesn't already exist. |
88 | * Image file is sought in the same way as pages - |
89 | * by traversing the class hierarchy and looking for |
90 | * resource with class name and .gif extension. |
91 | * @param obj |
92 | * @return File name. |
93 | * @throws Exception |
94 | */ |
95 | public String getImageName(Object obj) throws Exception { |
96 | String ret = classImages.get(obj.getClass()); |
97 | if (ret==null) { |
98 | ImageProvider imageProvider = ConvertingService.convert(obj, ImageProvider.class); |
99 | if (imageProvider!=null) { |
100 | File imageFile = new File(outputDir, IMAGES+File.separator+obj.getClass().getName()+"."+GIF); |
101 | if (!imageFile.getParentFile().exists()) { |
102 | imageFile.getParentFile().mkdirs(); |
103 | } |
104 | FileOutputStream imageFileOutputStream = new FileOutputStream(imageFile); |
105 | InputStream imageInputStream = ConvertingService.convert(imageProvider.getIcon(), InputStream.class); |
106 | if (imageInputStream!=null) { |
107 | new Pumper(imageInputStream, imageFileOutputStream, true).call(); |
108 | ret = imageFile.getName(); |
109 | classImages.put(obj.getClass(), ret); |
110 | return ret; |
111 | } |
112 | } |
113 | } |
114 | return ret==null ? "dhtmlgoodies_folder.gif" : ret; |
115 | } |
116 | |
117 | /** |
118 | * @param element |
119 | * @return Object ID. |
120 | */ |
121 | public Object getId(Object element) { |
122 | return identityManager.getIdentity(element); |
123 | } |
124 | |
125 | /** |
126 | * Renders outline for the current object to a writer. |
127 | * @param obj Source object. |
128 | * @param writer Output writer. |
129 | * @throws RenderingException |
130 | */ |
131 | public void renderOutline(Object obj, Writer writer) throws RenderingException { |
132 | renderOutline(obj, writer, false); |
133 | } |
134 | |
135 | /** |
136 | * Renders outline for the current object to a writer. |
137 | * @param obj Source object. |
138 | * @param writer Output writer. |
139 | * @param http If true outline shall be rendered with use |
140 | * of AJAX, which is useful for large trees. |
141 | * @throws RenderingException |
142 | */ |
143 | public void renderOutline(Object obj, Writer writer, boolean http) throws RenderingException { |
144 | WriterRenderer renderer = ConvertingService.convert(obj, WriterRenderer.class); |
145 | if (renderer!=null) { |
146 | renderer.render(writer, env, context, http ? "outline_http" : "outline", locale, getOutputDir()); |
147 | } |
148 | } |
149 | |
150 | /** |
151 | * Renders details and contents without rendering outline. |
152 | * This method can be used for model elements not appearing |
153 | * in the outline, but referenced by objects appearing in the |
154 | * outline. |
155 | * @param obj Source object. |
156 | * @param http If true outline shall be rendered with use |
157 | * of AJAX, which is useful for large trees. |
158 | * @throws RenderingException |
159 | */ |
160 | public void renderDetailsAndContents(Object obj) throws RenderingException { |
161 | if (obj!=null) { |
162 | IdentityManager<?> identityManager = context.lookup(IdentityManager.class); |
163 | Object id = identityManager.getIdentity(obj); |
164 | FileRenderer renderer = ConvertingService.convert(obj, FileRenderer.class); |
165 | if (renderer!=null) { |
166 | File detailsOut = new File(outputDir, "e"+id+".html"); |
167 | if (!detailsOut.exists()) { |
168 | renderer.render(detailsOut, env, context, null, locale); |
169 | } |
170 | File contentsOut = new File(outputDir, "e"+id+"_contents.html"); |
171 | if (!contentsOut.exists()) { |
172 | renderer.render(contentsOut, env, context, http ? CONTENTS_HTTP : CONTENTS, locale); |
173 | } |
174 | } else { |
175 | throw new NullPointerException("Renderer not found"); |
176 | } |
177 | } |
178 | } |
179 | |
180 | /** |
181 | * Renders details and contents without rendering outline. Returns |
182 | * link to the file and possibly anchor within the file. |
183 | * This method can be used for model elements not appearing |
184 | * in the outline, but referenced by objects appearing in the |
185 | * outline. |
186 | * @param obj Source object. |
187 | * @param http If true outline shall be rendered with use |
188 | * of AJAX, which is useful for large trees. |
189 | * @throws RenderingException |
190 | */ |
191 | public String renderAndLink(Object obj) throws RenderingException { |
192 | if (obj!=null) { |
193 | CompositePart part = ConvertingService.convert(obj, CompositePart.class, context, obj.getClass().getClassLoader()); |
194 | String tail=""; |
195 | if (part!=null) { |
196 | String anchor = part.getAnchor(); |
197 | if (anchor!=null) { |
198 | tail = "#"+part.getAnchor(); |
199 | } |
200 | obj = part.getComposite(); |
201 | } |
202 | IdentityManager<?> identityManager = context.lookup(IdentityManager.class); |
203 | Object id = identityManager.getIdentity(obj); |
204 | // int bucketNo = Math.abs(id.hashCode() % BUCKETS); |
205 | // File bucketDir = new File(outputDir, "D"+bucketNo); |
206 | // if (!bucketDir.exists()) { |
207 | // bucketDir.mkdirs(); |
208 | // } |
209 | FileRenderer renderer = ConvertingService.convert(obj, FileRenderer.class); |
210 | if (renderer!=null) { |
211 | File detailsOut = new File(outputDir, "e"+id+".html"); |
212 | if (!detailsOut.exists()) { |
213 | renderer.render(detailsOut, env, context, null, locale); |
214 | } |
215 | File contentsOut = new File(outputDir, "e"+id+"_contents.html"); |
216 | if (!contentsOut.exists()) { |
217 | renderer.render(contentsOut, env, context, http ? CONTENTS_HTTP : CONTENTS, locale); |
218 | } |
219 | return /* bucketDir.getName()+"/"+ */ detailsOut.getName()+tail; |
220 | } else { |
221 | throw new NullPointerException("Renderer not found"); |
222 | } |
223 | } |
224 | return null; |
225 | } |
226 | |
227 | /** |
228 | * Tries to render object "inline" (using WriterRenderer). If object cannot be converted |
229 | * to WriterRenderer, it gets converted to FileRenderer and a link is rendered. |
230 | * @param obj Source object. |
231 | * @throws RenderingException |
232 | */ |
233 | public String render(Object obj, String profile) throws RenderingException { |
234 | if (obj==null) { |
235 | return ""; |
236 | } |
237 | |
238 | WriterRenderer wr = ConvertingService.convert(obj, WriterRenderer.class); |
239 | if (wr!=null) { |
240 | StringWriter writer = new StringWriter(); |
241 | try { |
242 | boolean rendered; |
243 | try { |
244 | rendered = wr.render(writer, env, context, profile, locale, outputDir); |
245 | } finally { |
246 | writer.close(); |
247 | } |
248 | if (rendered) { |
249 | return writer.toString(); |
250 | } |
251 | } catch (IOException e) { |
252 | throw new RenderingException(e); |
253 | } |
254 | } |
255 | |
256 | // Default inlining - link. |
257 | return "<a href=\""+renderAndLink(obj)+"\">"+obj+"</a>"; |
258 | } |
259 | |
260 | public boolean isBlank(String str) { |
261 | return str==null || str.trim().length()==0; |
262 | } |
263 | |
264 | public String null2blank(String str) { |
265 | return str==null ? "" : str; |
266 | } |
267 | |
268 | public String escapeHtml(String txt) { |
269 | if (txt==null) { |
270 | return null; |
271 | } |
272 | |
273 | StringBuffer ret = new StringBuffer(); |
274 | char[] chars=txt.toCharArray(); |
275 | for (int i=0; i<chars.length; ++i) { |
276 | switch (chars[i]) { |
277 | case '<': |
278 | ret.append("<"); |
279 | break; |
280 | case '>': |
281 | ret.append(">"); |
282 | break; |
283 | case '&': |
284 | if (i<chars.length-1 && '#'==chars[i+1]) { // Do not double-escape (&#...;) |
285 | ret.append(chars[i]); |
286 | } else { |
287 | ret.append("&"); |
288 | } |
289 | break; |
290 | case '\'': |
291 | ret.append("'"); |
292 | break; |
293 | case '\\': |
294 | ret.append("\"); |
295 | break; |
296 | case '\"': |
297 | ret.append("""); |
298 | break; |
299 | default: |
300 | ret.append("&#"+((int) chars[i])+";"); |
301 | } |
302 | } |
303 | return ret.toString(); |
304 | } |
305 | |
306 | /** |
307 | * Creates object. This method is a workaround for JXP classloader issues. |
308 | * @param className |
309 | * @param classLoader |
310 | * @param args |
311 | * @return |
312 | * @throws ClassNotFoundException |
313 | * @throws SecurityException |
314 | * @throws IllegalAccessException |
315 | * @throws InstantiationException |
316 | */ |
317 | public Object createObject(String className, ClassLoader classLoader, Object... args) throws Exception { |
318 | Class<?> clazz = classLoader.loadClass(className); |
319 | if (args==null || args.length==0) { |
320 | return clazz.newInstance(); |
321 | } |
322 | |
323 | Z: for (Constructor<?> c: clazz.getConstructors()) { |
324 | Class<?>[] parameterTypes = c.getParameterTypes(); |
325 | if (parameterTypes.length==args.length) { |
326 | for (int i=0; i<parameterTypes.length; ++i) { |
327 | if (args[i]!=null && !parameterTypes[i].isInstance(args[i])) { |
328 | continue Z; |
329 | } |
330 | } |
331 | return c.newInstance(args); |
332 | } |
333 | } |
334 | |
335 | throw new NoSuchMethodException("Could not find appropriate constructor for "+className+" with "+args.length+" arguments."); |
336 | } |
337 | |
338 | /** |
339 | * JXP has issues with class loading. This method is a workaround. |
340 | * @param source Source object. |
341 | * @param className Target class. |
342 | * @return |
343 | * @throws ClassNotFoundException |
344 | */ |
345 | public Object convert(Object source, String targetType) throws ClassNotFoundException { |
346 | ClassLoader classLoader = DuckConverterFactory.getChildClassLoader(source.getClass().getClassLoader(), getClass().getClassLoader()); |
347 | if (classLoader == null) { |
348 | classLoader = getClass().getClassLoader(); |
349 | } |
350 | return ConvertingService.convert(source, classLoader.loadClass(targetType), classLoader); |
351 | } |
352 | |
353 | /** |
354 | * Sets object attribute. |
355 | * @param target Target object. |
356 | * @param name |
357 | * @param value |
358 | * @return previous attribute value. |
359 | */ |
360 | public Object setAttribute(Object target, String name, Object value) { |
361 | synchronized (attributes) { |
362 | Map<String, Object> am = attributes.get(target); |
363 | if (am==null) { |
364 | am = new HashMap<String, Object>(); |
365 | attributes.put(target, am); |
366 | } |
367 | return am.put(name, value); |
368 | } |
369 | } |
370 | |
371 | /** |
372 | * Sets object attribute if it was not present. |
373 | * @param target Target object. |
374 | * @param name |
375 | * @param value |
376 | */ |
377 | public Object setAttributeIfAbsent(Object target, String name, Object value) { |
378 | synchronized (attributes) { |
379 | Map<String, Object> am = attributes.get(target); |
380 | if (am==null) { |
381 | am = new HashMap<String, Object>(); |
382 | attributes.put(target, am); |
383 | } |
384 | if (!am.containsKey(name)) { |
385 | return am.put(name, value); |
386 | } |
387 | return null; |
388 | } |
389 | } |
390 | |
391 | /** |
392 | * Replaces object attribute value only if it is present. |
393 | * @param target Target object. |
394 | * @param name |
395 | * @param value |
396 | */ |
397 | public Object replaceAttribute(Object target, String name, Object value) { |
398 | synchronized (attributes) { |
399 | Map<String, Object> am = attributes.get(target); |
400 | if (am==null) { |
401 | am = new HashMap<String, Object>(); |
402 | attributes.put(target, am); |
403 | } |
404 | if (am.containsKey(name)) { |
405 | return am.put(name, value); |
406 | } |
407 | return null; |
408 | } |
409 | } |
410 | |
411 | public Object removeAttribute(Object target, String name) { |
412 | synchronized (attributes) { |
413 | Map<String, Object> am = attributes.get(target); |
414 | if (am!=null) { |
415 | return am.remove(name); |
416 | } |
417 | return null; |
418 | } |
419 | } |
420 | |
421 | public Object getAttribute(Object target, String name) { |
422 | synchronized (attributes) { |
423 | Map<String, Object> am = attributes.get(target); |
424 | if (am!=null) { |
425 | return am.get(name); |
426 | } |
427 | return null; |
428 | } |
429 | } |
430 | |
431 | public Object getAttribute(Object target, String name, String defaultValue) { |
432 | synchronized (attributes) { |
433 | Map<String, Object> am = attributes.get(target); |
434 | if (am!=null) { |
435 | return am.get(name); |
436 | } |
437 | return defaultValue; |
438 | } |
439 | } |
440 | |
441 | /** |
442 | * Renders Wiki style link url[|name] as HTML link. |
443 | * @param wikiLink |
444 | * @return |
445 | */ |
446 | public String wikiLink(String wikiLink) { |
447 | if (wikiLink==null) { |
448 | return ""; |
449 | } |
450 | |
451 | int idx = wikiLink.indexOf("|"); |
452 | if (idx==-1) { |
453 | return "<a href=\""+wikiLink+"\">"+wikiLink+"</a>"; |
454 | } |
455 | |
456 | return "<a href=\""+wikiLink.substring(0, idx)+"\">"+wikiLink.substring(idx+1)+"</a>"; |
457 | } |
458 | } |