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("&lt;");
279                                    break;
280                            case '>':
281                                    ret.append("&gt;");
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("&amp;");
288                                    }
289                                    break;
290                            case '\'':
291                                    ret.append("&#039;");
292                                    break;
293                            case '\\':
294                                    ret.append("&#092;");
295                                    break;
296                            case '\"':
297                                    ret.append("&quot;");
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    }