001 package com.hammurapi.render;
002
003 import java.io.File;
004 import java.io.FileOutputStream;
005 import java.io.IOException;
006 import java.io.InputStream;
007 import java.io.StringWriter;
008 import java.io.Writer;
009 import java.lang.reflect.Constructor;
010 import java.util.HashMap;
011 import java.util.Locale;
012 import java.util.Map;
013 import java.util.WeakHashMap;
014 import java.util.concurrent.ConcurrentHashMap;
015
016 import com.hammurapi.common.Context;
017 import com.hammurapi.common.IdentityManager;
018 import com.hammurapi.common.Pumper;
019 import com.hammurapi.convert.ConvertingService;
020 import com.hammurapi.convert.DuckConverterFactory;
021
022 /**
023 * Helper class for ReportGenerator and Jxp renderers.
024 * It is available in Jxp templates in ReportGenerator
025 * as <code>renderHelper</code> variable.
026 * @author Pavel Vlasov
027 *
028 */
029 public class RenderHelper implements RenderingConstants {
030
031 /**
032 * Keeps object attributes.
033 */
034 private static final Map<Object, Map<String, Object>> attributes = new WeakHashMap<Object, Map<String,Object>>();
035
036 // private static final int BUCKETS = 200;
037 private Map<String, Object> env;
038 private Context context;
039 private Locale locale;
040
041 private IdentityManager<?> identityManager;
042 private File outputDir;
043 private Map<Class<?>, String> classImages = new ConcurrentHashMap<Class<?>, String>();
044 private boolean http;
045
046 /**
047 * @return Output directory.
048 */
049 public File getOutputDir() {
050 return outputDir;
051 }
052
053 /**
054 * @return Identity manager.
055 */
056 public IdentityManager<?> getIdentityManager() {
057 return identityManager;
058 }
059
060 /**
061 * Constructor.
062 * @param identityManager Identity manager.
063 * @param outputDir Output directory.
064 * @param env Environment.
065 * @param context Context.
066 * @param locale Locale.
067 */
068 public RenderHelper(
069 IdentityManager<?> identityManager,
070 File outputDir,
071 Map<String, Object> env,
072 Context context,
073 Locale locale,
074 boolean http) {
075 super();
076 this.identityManager = identityManager;
077 this.outputDir = outputDir;
078 this.env = env;
079 this.context = context;
080 this.locale = locale;
081 this.http = http;
082 }
083
084 /**
085 * Finds image file for a given object in classloader,
086 * writes it to the "images" directory in the
087 * output directory, if it doesn't already exist.
088 * Image file is sought in the same way as pages -
089 * by traversing the class hierarchy and looking for
090 * resource with class name and .gif extension.
091 * @param obj
092 * @return File name.
093 * @throws Exception
094 */
095 public String getImageName(Object obj) throws Exception {
096 String ret = classImages.get(obj.getClass());
097 if (ret==null) {
098 ImageProvider imageProvider = ConvertingService.convert(obj, ImageProvider.class);
099 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 }