root/helma/helma/trunk/src/helma/framework/core/Skin.java

Revision 9983, 42.5 kB (checked in by hannes, 4 months ago)

Add skin introspection patch from Tobi Schäfer for bug #688
http://dev.helma.org/bugs/show_bug.cgi?id=688

  • Property svn:eol-style set to native
  • Property cvs2svn:cvs-rev set to 1.93
  • Property svn:keywords set to Date Revision Author HeadURL Id
Line 
1 /*
2  * Helma License Notice
3  *
4  * The contents of this file are subject to the Helma License
5  * Version 2.0 (the "License"). You may not use this file except in
6  * compliance with the License. A copy of the License is available at
7  * http://adele.helma.org/download/helma/license.txt
8  *
9  * Copyright 1998-2003 Helma Software. All Rights Reserved.
10  *
11  * $RCSfile$
12  * $Author$
13  * $Revision$
14  * $Date$
15  */
16
17 package helma.framework.core;
18
19 import helma.framework.*;
20 import helma.framework.repository.Resource;
21 import helma.objectmodel.ConcurrencyException;
22 import helma.util.*;
23 import helma.scripting.ScriptingEngine;
24
25 import java.util.*;
26 import java.io.UnsupportedEncodingException;
27 import java.io.Reader;
28 import java.io.InputStreamReader;
29 import java.io.IOException;
30
31 /**
32  * This represents a Helma skin, i.e. a template created from containing Macro tags
33  * that will be dynamically evaluated.. It uses the request path array
34  * from the RequestEvaluator object to resolve Macro handlers by type name.
35  */
36 public final class Skin {
37
38     private Macro[] macros;
39     private Application app;
40     private char[] source;
41     private int offset, length; // start and end index of skin content
42     private HashSet sandbox;
43     private HashMap subskins;
44     private Skin parentSkin = this;
45     private String extendz = null;
46     private boolean hasContent = false;
47
48     static private final int PARSE_MACRONAME = 0;
49     static private final int PARSE_PARAM = 1;
50     static private final int PARSE_DONE = 2;
51
52     static private final int ENCODE_NONE = 0;
53     static private final int ENCODE_HTML = 1;
54     static private final int ENCODE_XML = 2;
55     static private final int ENCODE_FORM = 3;
56     static private final int ENCODE_URL = 4;
57     static private final int ENCODE_ALL = 5;
58
59     static private final int HANDLER_RESPONSE = 0;
60     static private final int HANDLER_REQUEST = 1;
61     static private final int HANDLER_SESSION = 2;
62     static private final int HANDLER_PARAM = 3;
63     static private final int HANDLER_GLOBAL = 4;
64     static private final int HANDLER_THIS = 5;
65     static private final int HANDLER_OTHER = 6;
66
67     static private final int FAIL_DEFAULT = 0;
68     static private final int FAIL_SILENT = 1;
69     static private final int FAIL_VERBOSE = 2;
70
71     /**
72      * Create a skin without any restrictions on which macros are allowed to be called from it
73      */
74     public Skin(String content, Application app) {
75         this.app = app;
76         this.sandbox = null;
77         this.source = content.toCharArray();
78         this.offset = 0;
79         this.length = source.length;
80         parse();
81     }
82
83     /**
84      * Create a skin with a sandbox which contains the names of macros allowed to be called
85      */
86     public Skin(String content, Application app, HashSet sandbox) {
87         this.app = app;
88         this.sandbox = sandbox;
89         this.source = content.toCharArray();
90         this.offset = 0;
91         length = source.length;
92         parse();
93     }
94
95     /**
96      *  Create a skin without any restrictions on the macros from a char array.
97      */
98     public Skin(char[] content, int length, Application app) {
99         this.app = app;
100         this.sandbox = null;
101         this.source = content;
102         this.offset = 0;
103         this.length = length;
104         parse();
105     }
106
107     /**
108      *  Subskin constructor.
109      */
110     private Skin(Skin parentSkin, Macro anchorMacro) {
111         this.parentSkin = parentSkin;
112         this.app = parentSkin.app;
113         this.sandbox = parentSkin.sandbox;
114         this.source = parentSkin.source;
115         this.offset = anchorMacro.end;
116         this.length = parentSkin.length;
117         parentSkin.addSubskin(anchorMacro.name, this);
118         parse();
119     }
120
121     public static Skin getSkin(Resource res, Application app) throws IOException {
122         String encoding = app.getProperty("skinCharset");
123         Reader reader;
124         if (encoding == null) {
125             reader = new InputStreamReader(res.getInputStream());
126         } else {
127             reader = new InputStreamReader(res.getInputStream(), encoding);
128         }
129
130         int length = (int) res.getLength();
131         char[] characterBuffer = new char[length];
132         int read = 0;
133         try {
134             while (read < length) {
135                 int r = reader.read(characterBuffer, read, length - read);
136                 if (r == -1)
137                     break;
138                 read += r;
139             }
140         } finally {
141             reader.close();
142         }
143         return new Skin(characterBuffer, read, app);
144     }
145
146     /**
147      * Parse a skin object from source text
148      */
149     private void parse() {
150         ArrayList partBuffer = new ArrayList();
151
152         boolean escape = false;
153         for (int i = offset; i < (length - 1); i++) {
154             if (source[i] == '<' && source[i + 1] == '%' && !escape) {
155                 // found macro start tag
156                 Macro macro = new Macro(i, 2);
157                 if (macro.isSubskinMacro) {
158                     new Skin(parentSkin, macro);
159                     length = i;
160                     break;
161                 } else {
162                     if (!macro.isCommentMacro) {
163                         hasContent = true;
164                     }
165                     partBuffer.add(macro);
166                 }
167                 i = macro.end - 1;
168             } else {
169                 if (!hasContent && !Character.isWhitespace(source[i])){
170                     hasContent = true;
171                 }
172                 escape = source[i] == '\\' && !escape;
173             }
174         }
175
176         macros = new Macro[partBuffer.size()];
177         partBuffer.toArray(macros);
178     }
179
180     private void addSubskin(String name, Skin subskin) {
181         if (subskins == null) {
182             subskins = new HashMap();
183         }
184         subskins.put(name, subskin);
185     }
186    
187     /**
188      * Return the list of macros found by the parser
189      * @return the list of macros
190      */
191     public Macro[] getMacros() {
192         return macros;
193     }
194
195     /**
196      * Check if this skin has a main skin, as opposed to consisting just of subskins
197      * @return true if this skin contains a main skin
198      */
199     public boolean hasMainskin() {
200         return hasContent;
201     }
202
203     /**
204      * Check if this skin contains a subskin with the given name
205      * @param name a subskin name
206      * @return true if the given subskin exists
207      */
208     public boolean hasSubskin(String name) {
209         return subskins != null && subskins.containsKey(name);
210     }
211
212     /**
213      * Get a subskin by name
214      * @param name the subskin name
215      * @return the subskin
216      */
217     public Skin getSubskin(String name) {
218         return subskins == null ? null : (Skin) subskins.get(name);
219     }
220
221     /**
222      * Return an array of subskin names defined in this skin
223      * @return a string array containing this skin's substrings
224      */
225     public String[] getSubskinNames() {
226         return subskins == null ?
227                 new String[0] :
228                 (String[]) subskins.keySet().toArray(new String[0]);
229     }
230
231     public String getExtends() {
232         return extendz;
233     }
234
235     /**
236      * Get the raw source text this skin was parsed from
237      */
238     public String getSource() {
239         return new String(source, offset, length - offset);
240     }
241
242     /**
243      * Render this skin and return it as string
244      */
245     public String renderAsString(RequestEvaluator reval, Object thisObject, Object paramObject)
246                 throws RedirectException, UnsupportedEncodingException {
247         String result = "";
248         ResponseTrans res = reval.getResponse();
249         res.pushBuffer(null);
250         try {
251             render(reval, thisObject, paramObject);
252         } finally {
253             result = res.popString();
254         }
255         return result;
256     }
257
258
259     /**
260      * Render this skin
261      */
262     public void render(RequestEvaluator reval, Object thisObject, Object paramObject)
263                 throws RedirectException, UnsupportedEncodingException {
264         // check for endless skin recursion
265         if (++reval.skinDepth > 50) {
266             throw new RuntimeException("Recursive skin invocation suspected");
267         }
268
269         ResponseTrans res = reval.getResponse();
270
271         if (macros == null) {
272             res.write(source, offset, length - offset);
273             reval.skinDepth--;
274             return;
275         }
276
277         // register param object, remember previous one to reset afterwards
278         Map handlers = res.getMacroHandlers();
279         Object previousParam = handlers.put("param", paramObject);
280         Skin previousSkin = res.switchActiveSkin(parentSkin);
281
282         try {
283             int written = offset;
284             Map handlerCache = null;
285
286             if (macros.length > 3) {
287                 handlerCache = new HashMap();
288             }
289             RenderContext cx = new RenderContext(reval, thisObject, handlerCache);
290
291             for (int i = 0; i < macros.length; i++) {
292                 if (macros[i].start > written) {
293                     res.write(source, written, macros[i].start - written);
294                 }
295
296                 macros[i].render(cx);
297                 written = macros[i].end;
298             }
299
300             if (written < length) {
301                 res.write(source, written, length - written);
302             }
303         } finally {
304             reval.skinDepth--;
305             res.switchActiveSkin(previousSkin);
306             if (previousParam == null) {
307                 handlers.remove("param");
308             } else {
309                 handlers.put("param", previousParam);
310             }
311         }
312     }
313
314     /**
315      * Check if a certain macro is present in this skin. The macro name is in handler.name notation
316      */
317     public boolean containsMacro(String macroname) {
318         for (int i = 0; i < macros.length; i++) {
319             if (macros[i] instanceof Macro) {
320                 Macro m = macros[i];
321
322                 if (macroname.equals(m.name)) {
323                     return true;
324                 }
325             }
326         }
327
328         return false;
329     }
330
331     /**
332      *  Adds a macro to the list of allowed macros. The macro is in handler.name notation.
333      */
334     public void allowMacro(String macroname) {
335         if (sandbox == null) {
336             sandbox = new HashSet();
337         }
338
339         sandbox.add(macroname);
340     }
341
342     private Object processParameter(Object value, RenderContext cx)
343             throws Exception {
344         if (value instanceof Macro) {
345             return ((Macro) value).invokeAsParameter(cx);
346         } else {
347             return value;
348         }
349     }
350
351     public class Macro {
352         public final int start, end;
353         String name;
354         String[] path;
355         int handlerType = HANDLER_OTHER;
356         int encoding = ENCODE_NONE;
357         boolean hasNestedMacros = false;
358
359         // default render parameters - may be overridden if macro changes
360         // param.prefix/suffix/default
361         StandardParams standardParams = new StandardParams();
362         Map namedParams = null;
363         List positionalParams = null;
364         // filters defined via <% foo | bar %>
365         Macro filterChain;
366
367         // comment macros are silently dropped during rendering
368         boolean isCommentMacro = false;
369         // subskin macros delimits the beginning of a new subskin
370         boolean isSubskinMacro = false;
371
372         /**
373          * Create and parse a new macro.
374          * @param start the start of the macro within the skin source
375          * @param macroOffset offset of the macro content from the start index
376          */
377         Macro(int start, int macroOffset) {
378             this.start = start;
379
380             int i = parse(macroOffset, false);
381
382             if (isSubskinMacro) {
383                 if (i + 1 < length && source[i] == '\r' && source[i + 1] == '\n')
384                     end = Math.min(length, i + 2);
385                 else if (i < length && (source[i] == '\r' || source[i] == '\n'))
386                     end = Math.min(length, i + 1);
387                 else
388                     end = Math.min(length, i);
389             } else {
390                 end = Math.min(length, i);
391             }
392
393             path = StringUtils.split(name, ".");
394             if (path.length <= 1) {
395                 handlerType = HANDLER_GLOBAL;
396             } else {
397                 String handlerName = path[0];
398                 if ("this".equalsIgnoreCase(handlerName)) {
399                     handlerType = HANDLER_THIS;
400                 } else if ("response".equalsIgnoreCase(handlerName)) {
401                     handlerType = HANDLER_RESPONSE;
402                 } else if ("request".equalsIgnoreCase(handlerName)) {
403                     handlerType = HANDLER_REQUEST;
404                 } else if ("session".equalsIgnoreCase(handlerName)) {
405                     handlerType = HANDLER_SESSION;
406                 } else if ("param".equalsIgnoreCase(handlerName)) {
407                     handlerType = HANDLER_PARAM;
408                 }
409             }
410
411             if (".extends".equals(name)) {
412                 if (parentSkin != Skin.this) {
413                     throw new RuntimeException("Found .extends in subskin");
414                 }
415                 if (positionalParams == null || positionalParams.size() < 1
416                         || !(positionalParams.get(0) instanceof String)) {
417                     throw new RuntimeException(".extends requires an unnamed string parameter");
418                 }
419                 extendz = (String) positionalParams.get(0);
420                 isCommentMacro = true; // don't render
421             }
422         }
423
424         private int parse(int macroOffset, boolean lenient) {
425             int state = PARSE_MACRONAME;
426             boolean escape = false;
427             char quotechar = '\u0000';
428             String lastParamName = null;
429             StringBuffer b = new StringBuffer();
430             int i;
431
432             loop:
433             for (i = start + macroOffset; i < length - 1; i++) {
434
435                 switch (source[i]) {
436
437                     case '<':
438
439                         if (state == PARSE_PARAM && quotechar == '\u0000'
440                                 && b.length() == 0 && source[i + 1] == '%') {
441                             Macro macro = new Macro(i, 2);
442                             addParameter(lastParamName, macro);
443                             lastParamName = null;
444                             b.setLength(0);
445                             i = macro.end - 1;
446                         } else {
447                             b.append(source[i]);
448                             escape = false;
449                         }
450                         break;
451
452                     case '%':
453
454                         if ((state != PARSE_PARAM || quotechar == '\u0000' || lenient)
455                                 && source[i + 1] == '>') {
456                             state = PARSE_DONE;
457                             break loop;
458                         }
459                         b.append(source[i]);
460                         escape = false;
461                         break;
462
463                     case '/':
464
465                         b.append(source[i]);
466                         escape = false;
467
468                         if (state == PARSE_MACRONAME && "//".equals(b.toString())) {
469                             isCommentMacro = true;
470                             // just continue parsing the macro as this is the only way
471                             // to correctly catch embedded macros - see bug 588
472                         }
473                         break;
474
475                     case '#':
476
477                         if (state == PARSE_MACRONAME && b.length() == 0) {
478                             // this is a subskin/skinlet
479                             isSubskinMacro = true;
480                             break;
481                         }
482                         b.append(source[i]);
483                         escape = false;
484                         break;
485
486                     case '|':
487
488                         if (!escape && quotechar == '\u0000') {
489                             filterChain = new Macro(i, 1);
490                             i = filterChain.end - 2;
491                             lastParamName = null;
492                             b.setLength(0);
493                             state = PARSE_DONE;
494                             break loop;
495                         }
496                         b.append(source[i]);
497                         escape = false;
498                         break;
499
500                     case '\\':
501
502                         if (escape) {
503                             b.append(source[i]);
504                         }
505
506                         escape = !escape;
507
508                         break;
509
510                     case '"':
511                     case '\'':
512
513                         if (!escape && state == PARSE_PARAM) {
514                             if (quotechar == source[i]) {
515                                 if (source[i + 1] != '%' && !Character.isWhitespace(source[i + 1]) && !lenient) {
516                                     // closing quotes and next character is not space or end tag -
517                                     // switch to lenient mode
518                                     reset();
519                                     return parse(macroOffset, true);
520                                 }
521                                 // add parameter
522                                 addParameter(lastParamName, b.toString());
523                                 lastParamName = null;
524                                 b.setLength(0);
525                                 quotechar = '\u0000';
526                             } else if (quotechar == '\u0000') {
527                                 quotechar = source[i];
528                                 b.setLength(0);
529                             } else {
530                                 b.append(source[i]);
531                             }
532                         } else {
533                             b.append(source[i]);
534                         }
535
536                         escape = false;
537
538                         break;
539
540                     case ' ':
541                     case '\t':
542                     case '\n':
543                     case '\r':
544                     case '\f':
545
546                         if (state == PARSE_MACRONAME && b.length() > 0) {
547                             name = b.toString().trim();
548                             b.setLength(0);
549                             state = PARSE_PARAM;
550                         } else if (state == PARSE_PARAM) {
551                             if (quotechar == '\u0000') {
552                                 if (b.length() > 0) {
553                                     // add parameter
554                                     addParameter(lastParamName, b.toString());
555                                     lastParamName = null;
556                                     b.setLength(0);
557                                 }
558                             } else {
559                                 b.append(source[i]);
560                                 escape = false;
561                             }
562                         }
563
564                         break;
565
566                     case '=':
567
568                         if (!escape && quotechar == '\u0000' && state == PARSE_PARAM && lastParamName == null) {
569                             lastParamName = b.toString().trim();
570                             b.setLength(0);
571                         } else {
572                             b.append(source[i]);
573                             escape = false;
574                         }
575
576                         break;
577
578                     default:
579                         b.append(source[i]);
580                         escape = false;
581                 }
582
583                 if (i == length - 2 && !lenient &&
584                         (state != PARSE_DONE ||quotechar != '\u0000')) {
585                     // macro tag is not properly terminated, switch to lenient mode                   
586                     reset();
587                     return parse(macroOffset, true);
588                 }
589             }
590
591             if (b.length() > 0) {
592                 if (name == null) {
593                     name = b.toString().trim();
594                 } else {
595                     addParameter(lastParamName, b.toString());
596                 }
597             }
598
599             if (state != PARSE_DONE) {
600                 app.logError("Unterminated Macro Tag: " +this);
601             }
602
603             return i + 2;
604         }
605
606         private void reset() {
607             filterChain = null;
608             name = null;
609             standardParams = new StandardParams();
610             namedParams = null;
611             positionalParams = null;
612         }
613
614         private void addParameter(String name, Object value) {
615             if (!(value instanceof String)) {
616                 hasNestedMacros = true;               
617             }
618             if (name == null) {
619                 // take shortcut for positional parameters
620                 if (positionalParams == null) {
621                     positionalParams = new ArrayList();
622                 }
623                 positionalParams.add(value);
624                 return;
625             }
626             // check if this is parameter is relevant to us
627             if ("prefix".equals(name)) {
628                 standardParams.prefix = value;
629             } else if ("suffix".equals(name)) {
630                 standardParams.suffix = value;
631             } else if ("encoding".equals(name)) {
632                 if ("html".equals(value)) {
633                     encoding = ENCODE_HTML;
634                 } else if ("xml".equals(value)) {
635                     encoding = ENCODE_XML;
636                 } else if ("form".equals(value)) {
637                     encoding = ENCODE_FORM;
638                 } else if ("url".equals(value)) {
639                     encoding = ENCODE_URL;
640                 } else if ("all".equals(value)) {
641                     encoding = ENCODE_ALL;
642                 } else {
643                     app.logEvent("Unrecognized encoding in skin macro: " + value);
644                 }
645             } else if ("default".equals(name)) {
646                 standardParams.defaultValue = value;
647             } else if ("failmode".equals(name)) {
648                 standardParams.setFailMode(value);
649             }
650
651             // Add parameter to parameter map
652             if (namedParams == null) {
653                 namedParams = new HashMap();
654             }
655             namedParams.put(name, value);
656         }
657
658         private Object invokeAsMacro(RenderContext cx, StandardParams stdParams, boolean asObject)
659                 throws Exception {
660
661             // immediately return for comment macros
662             if (isCommentMacro || name == null) {
663                 return null;
664             }
665
666             if ((sandbox != null) && !sandbox.contains(name)) {
667                 throw new MacroException("Macro not allowed in sandbox: " + name);
668             }
669
670             Object handler = null;
671             Object value = null;
672             ScriptingEngine engine = cx.reval.scriptingEngine;
673
674             if (handlerType != HANDLER_GLOBAL) {
675                 handler = cx.resolveHandler(path[0], handlerType);
676                 handler = resolvePath(handler, cx.reval);
677             }
678
679             if (handlerType == HANDLER_GLOBAL || handler != null) {
680                 // check if a function called name_macro is defined.
681                 // if so, the macro evaluates to the function. Otherwise,
682                 // a property/field with the name is used, if defined.
683                 String propName = path[path.length - 1];
684                 String funcName = resolveFunctionName(handler, propName + "_macro", engine);
685
686                 // remember length of response buffer before calling macro
687                 StringBuffer buffer = cx.reval.getResponse().getBuffer();
688                 int bufLength = buffer.length();
689
690                 if (funcName != null) {
691
692                     Object[] arguments = prepareArguments(0, cx);
693                     // get reference to rendered named params for after invocation
694                     Map params = (Map) arguments[0];
695                     value = cx.reval.invokeDirectFunction(handler,
696                             funcName,
697                             arguments);
698
699                     // update StandardParams to override defaults in case the macro changed anything
700                     if (stdParams != null) stdParams.readFrom(params);
701
702                     // if macro has a filter chain and didn't return anything, use output
703                     // as filter argument.
704                     if (asObject && value == null && buffer.length() > bufLength) {
705                         value = buffer.substring(bufLength);
706                         buffer.setLength(bufLength);
707                     }
708
709                     return filter(value, cx);
710                 } else {
711                     if (handlerType == HANDLER_RESPONSE) {
712                         // some special handling for response handler
713                         if ("message".equals(propName))
714                             value = cx.reval.getResponse().getMessage();
715                         else if ("error".equals(propName))
716                             value = cx.reval.getResponse().getErrorMessage();
717                         if (value != null)
718                             return filter(value, cx);
719                     }
720                     // display error message unless onUnhandledMacro is defined or silent failmode is on
721                     if (!engine.hasProperty(handler, propName)) {
722                         if (engine.hasFunction(handler, "onUnhandledMacro", false)) {
723                             Object[] arguments = prepareArguments(1, cx);
724                             arguments[0] = propName;
725                             value = cx.reval.invokeDirectFunction(handler,  "onUnhandledMacro", arguments);
726                             // if macro has a filter chain and didn't return anything, use output
727                             // as filter argument.
728                             if (asObject && value == null && buffer.length() > bufLength) {
729                                 value = buffer.substring(bufLength);
730                                 buffer.setLength(bufLength);
731                             }
732                         } else if (standardParams.verboseFailmode(handler, engine)) {
733                             throw new MacroException("Unhandled macro: " + name);
734                         }
735                     } else {
736                         value = engine.getProperty(handler, propName);
737                     }
738                     return filter(value, cx);
739                 }
740             } else if (standardParams.verboseFailmode(handler, engine)) {
741                 throw new MacroException("Unhandled macro: " + name);
742             }
743             return filter(null, cx);
744         }
745
746         /**
747          * Render this macro as nested macro, only converting to string
748          * if necessary.
749          */
750         Object invokeAsParameter(RenderContext cx) throws Exception {
751             StandardParams stdParams = standardParams.render(cx);           
752             Object value = invokeAsMacro(cx, stdParams, true);
753             if (stdParams.prefix != null || stdParams.suffix != null) {
754                 ResponseTrans res = cx.reval.getResponse();
755                 res.pushBuffer(null);
756                 writeResponse(value, cx.reval, stdParams, true);
757                 return res.popString();
758             } else if (stdParams.defaultValue != null &&
759                     (value == null || "".equals(value))) {
760                 return stdParams.defaultValue;
761             } else {
762                 return value;
763             }
764         }
765
766         /**
767          *  Render the macro given a handler object.
768          */
769         void render(RenderContext cx)
770                 throws RedirectException, UnsupportedEncodingException {
771             StringBuffer buffer = cx.reval.getResponse().getBuffer();
772             // remember length of response buffer before calling macro
773             int bufLength = buffer.length();
774             try {
775                 StandardParams stdParams = standardParams.render(cx);
776                 boolean asObject = filterChain != null;
777                 Object value = invokeAsMacro(cx, stdParams, asObject);
778
779                 // check if macro wrote out to response buffer
780                 if (buffer.length() == bufLength) {
781                     // If the macro function didn't write anything to the response itself,
782                     // we interpret its return value as macro output.
783                     writeResponse(value, cx.reval, stdParams, true);
784                 } else {
785                     // if an encoding is specified, re-encode the macro's output
786                     if (encoding != ENCODE_NONE) {
787                         String output = buffer.substring(bufLength);
788
789                         buffer.setLength(bufLength);
790                         writeResponse(output, cx.reval, stdParams, false);
791                     } else {
792                         // insert prefix,
793                         if (stdParams.prefix != null) {
794                             buffer.insert(bufLength, stdParams.prefix);
795                         }
796                         // append suffix
797                         if (stdParams.suffix != null) {
798                             buffer.append(stdParams.suffix);
799                         }
800                     }
801
802                     // Append macro return value even if it wrote something to the response,
803                     // but don't render default value in case it returned nothing.
804                     // We do this for the sake of consistency.
805                     writeResponse(value, cx.reval, stdParams, false);
806                 }
807
808             } catch (RedirectException redir) {
809                 throw redir;
810             } catch (ConcurrencyException concur) {
811                 throw concur;
812             } catch (TimeoutException timeout) {
813                 throw timeout;
814             } catch (MacroException mx) {
815                 String msg = mx.getMessage();
816                 cx.reval.getResponse().write(" [" + msg + "] ");
817                 app.logError(msg);
818             } catch (Exception x) {
819                 String msg = x.getMessage();
820                 if ((msg == null) || (msg.length() < 10)) {
821                     msg = x.toString();
822                 }
823                 msg = new StringBuffer("Macro error in ").append(name)
824                         .append(": ").append(msg).toString();
825                 cx.reval.getResponse().write(" [" + msg + "] ");
826                 app.logError(msg, x);
827             }
828         }
829
830         private Object filter(Object returnValue, RenderContext cx)
831                 throws Exception {
832             // invoke filter chain if defined
833             if (filterChain != null) {
834                 return filterChain.invokeAsFilter(returnValue, cx);
835             } else {
836                 return returnValue;
837             }
838         }
839
840         private Object invokeAsFilter(Object returnValue, RenderContext cx)
841                 throws Exception {
842
843             if (name == null) {
844                 throw new MacroException("Empty macro filter");
845             } else if (sandbox != null && !sandbox.contains(name)) {
846                 throw new MacroException("Macro not allowed in sandbox: " + name);
847             }
848             Object handlerObject = null;
849
850             if (handlerType != HANDLER_GLOBAL) {
851                 handlerObject = cx.resolveHandler(path[0], handlerType);
852                 handlerObject = resolvePath(handlerObject, cx.reval);
853             }
854
855             String propName = path[path.length - 1] + "_filter";
856             String funcName = resolveFunctionName(handlerObject, propName,
857                     cx.reval.scriptingEngine);
858
859             if (funcName != null) {
860                 Object[] arguments = prepareArguments(1, cx);
861                 arguments[0] = returnValue;
862                 Object retval = cx.reval.invokeDirectFunction(handlerObject,
863                                                            funcName,
864                                                            arguments);
865
866                 return filter(retval, cx);
867             } else {
868                 throw new MacroException("Undefined macro filter: " + name);
869             }
870         }
871
872         private Object[] prepareArguments(int offset, RenderContext cx)
873                 throws Exception {
874             int nPosArgs = (positionalParams == null) ? 0 : positionalParams.size();
875             Object[] arguments = new Object[offset + 1 + nPosArgs];
876
877             if (namedParams == null) {
878                 arguments[offset] = new SystemMap(4);
879             } else if (hasNestedMacros) {
880                 SystemMap map = new SystemMap((int) (namedParams.size() * 1.5));
881                 for (Iterator it = namedParams.entrySet().iterator(); it.hasNext(); ) {
882                     Map.Entry entry = (Map.Entry) it.next();
883                     Object value = entry.getValue();
884                     if (!(value instanceof String))
885                         value = processParameter(value, cx);
886                     map.put(entry.getKey(), value);
887                 }
888                 arguments[offset] = map;
889             } else {
890                 // pass a clone/copy of the parameter map so if the script changes it,
891                 arguments[offset] = new CopyOnWriteMap(namedParams);
892             }
893             if (positionalParams != null) {
894                 for (int i = 0; i < nPosArgs; i++) {
895                     Object value = positionalParams.get(i);
896                     if (!(value instanceof String))
897                         value = processParameter(value, cx);
898                     arguments[offset + 1 + i] = value;
899                 }
900             }
901             return arguments;
902         }
903
904         private Object resolvePath(Object handler, RequestEvaluator reval) throws Exception {
905             for (int i = 1; i < path.length - 1; i++) {
906                 Object[] arguments = {path[i]};
907                 Object next = reval.invokeDirectFunction(handler, "getMacroHandler", arguments);
908                 if (next != null) {
909                     handler = next;
910                 } else if (!reval.scriptingEngine.isTypedObject(handler)) {
911                     handler = reval.scriptingEngine.getProperty(handler, path[i]);
912                     if (handler == null) {
913                         return null;
914                     }
915                 } else {
916                     return null;
917                 }
918             }
919             return handler;
920         }
921
922         private String resolveFunctionName(Object handler, String functionName,
923                                            ScriptingEngine engine) {
924             if (handlerType == HANDLER_GLOBAL) {
925                 String[] macroPath = app.globalMacroPath;
926                 if (macroPath == null || macroPath.length == 0) {
927                     if (engine.hasFunction(null, functionName, false))
928                         return functionName;
929                 } else {
930                     for (int i = 0; i < macroPath.length; i++) {
931                         String path = macroPath[i];
932                         String funcName = path == null || path.length() == 0 ?
933                                 functionName : path + "." + functionName;
934                         if (engine.hasFunction(null, funcName, true))
935                             return funcName;
936                     }
937                 }
938             } else {
939                 if (engine.hasFunction(handler, functionName, false))
940                     return functionName;
941             }
942             return null;
943         }
944
945         /**
946          * Utility method for writing text out to the response object.
947          */
948         void writeResponse(Object value, RequestEvaluator reval,
949                            StandardParams stdParams, boolean useDefault)
950                 throws Exception {
951             String text;
952             StringBuffer buffer = reval.getResponse().getBuffer();
953
954             if (value == null || "".equals(value)) {
955                 if (useDefault) {
956                     text = (String) stdParams.defaultValue;
957                 } else {
958                     return;
959                 }
960             } else {
961                 text = reval.scriptingEngine.toString(value);
962             }
963
964             if ((text != null) && (text.length() > 0)) {
965                 // only write prefix/suffix if value is not null, if we write the default
966                 // value provided by the macro tag, we assume it's already complete
967                 if (stdParams.prefix != null && value != null) {
968                     buffer.append(stdParams.prefix);
969                 }
970
971                 switch (encoding) {
972                     case ENCODE_NONE:
973                         buffer.append(text);
974
975                         break;
976
977                     case ENCODE_HTML:
978                         HtmlEncoder.encode(text, buffer);
979
980                         break;
981
982                     case ENCODE_XML:
983                         HtmlEncoder.encodeXml(text, buffer);
984
985                         break;
986
987                     case ENCODE_FORM:
988                         HtmlEncoder.encodeFormValue(text, buffer);
989
990                         break;
991
992                     case ENCODE_URL:
993                         buffer.append(UrlEncoded.encode(text, app.charset));
994
995                         break;
996
997                     case ENCODE_ALL:
998                         HtmlEncoder.encodeAll(text, buffer);
999
1000                         break;
1001                 }
1002
1003                 if (stdParams.suffix != null && value != null) {
1004                     buffer.append(stdParams.suffix);
1005                 }
1006             }
1007         }
1008
1009         public String toString() {
1010             return "[Macro: " + name + "]";
1011         }
1012
1013         /**
1014          * Return the full name of the macro in handler.name notation
1015          * @return the macro name
1016          */
1017         public String getName() {
1018             return name;
1019         }
1020
1021         /**
1022          * Return the numeric type of the macro handler
1023          * @return the handler type
1024          */
1025         public int getHandlerType() {
1026                 return handlerType;
1027         }
1028
1029         /**
1030          * Return the list of named parameters
1031          * @return the list of named parameters
1032          */
1033         public Map getNamedParams() {
1034                 return namedParams;
1035         }
1036        
1037         /**
1038          * Return the list of positional parameters
1039          * @return the list of positional parameters
1040          */
1041         public List getPositionalParams() {
1042                 return positionalParams;
1043         }
1044        
1045         public boolean hasNestedMacros() {
1046                 return hasNestedMacros;
1047         }
1048
1049     }
1050
1051     class StandardParams {
1052         Object prefix = null;
1053         Object suffix = null;
1054         Object defaultValue = null;
1055         int failmode = FAIL_DEFAULT;
1056
1057         StandardParams() {}
1058
1059         StandardParams(Map map) {
1060             readFrom(map);
1061         }
1062
1063         void readFrom(Map map) {
1064             prefix = map.get("prefix");
1065             suffix = map.get("suffix");
1066             defaultValue = map.get("default");
1067         }
1068
1069         boolean containsMacros() {
1070             return !(prefix instanceof String)
1071                 || !(suffix instanceof String)
1072                 || !(defaultValue instanceof String);
1073         }
1074
1075         void setFailMode(Object value) {
1076             if ("silent".equals(value))
1077                 failmode = FAIL_SILENT;
1078             else if ("verbose".equals(value))
1079                 failmode = FAIL_VERBOSE;
1080             else if (value != null)
1081                 app.logEvent("unrecognized failmode value: " + value);
1082         }
1083
1084         boolean verboseFailmode(Object handler, ScriptingEngine engine) {
1085             return (failmode == FAIL_VERBOSE) ||
1086                    (failmode == FAIL_DEFAULT &&
1087                        (handler == null ||
1088                         engine.isTypedObject(handler)));
1089         }
1090
1091         StandardParams render(RenderContext cx)
1092                 throws Exception {
1093             if (!containsMacros())
1094                 return this;
1095             StandardParams stdParams = new StandardParams();
1096             stdParams.prefix = renderToString(prefix, cx);
1097             stdParams.suffix = renderToString(suffix, cx);
1098             stdParams.defaultValue = renderToString(defaultValue, cx);
1099             return stdParams;
1100         }
1101
1102         String renderToString(Object obj, RenderContext cx) throws Exception {
1103             Object value = processParameter(obj, cx);
1104             if (value == null)
1105                 return null;
1106             else if (value instanceof String)
1107                 return (String) value;
1108             else
1109                 return cx.reval.scriptingEngine.toString(value);
1110         }
1111
1112     }
1113
1114     class RenderContext {
1115         final RequestEvaluator reval;
1116         final Object thisObject;
1117         final Map handlerCache;
1118
1119         RenderContext(RequestEvaluator reval, Object thisObject, Map handlerCache) {
1120             this.reval = reval;
1121             this.thisObject = thisObject;
1122             this.handlerCache = handlerCache;
1123         }
1124
1125         private Object resolveHandler(String handlerName, int handlerType) {
1126             switch (handlerType) {
1127                 case HANDLER_THIS:
1128                     return thisObject;
1129                 case HANDLER_RESPONSE:
1130                     return reval.getResponse().getResponseData();
1131                 case HANDLER_REQUEST:
1132                     return reval.getRequest().getRequestData();
1133                 case HANDLER_SESSION:
1134                     return reval.getSession().getCacheNode();
1135             }
1136
1137             // try to get handler from handlerCache first
1138             if (handlerCache != null && handlerCache.containsKey(handlerName)) {
1139                 return handlerCache.get(handlerName);
1140             }
1141
1142             // if handler object wasn't found in cache first check this-object
1143             if (thisObject != null) {
1144                 // not a global macro - need to find handler object
1145                 // was called with this object - check this-object for matching prototype
1146                 Prototype proto = app.getPrototype(thisObject);
1147
1148                 if (proto != null && proto.isInstanceOf(handlerName)) {
1149                     return cacheHandler(handlerName, thisObject);
1150                 }
1151             }
1152
1153             // next look in res.handlers
1154             Map macroHandlers = reval.getResponse().getMacroHandlers();
1155             Object obj = macroHandlers.get(handlerName);
1156             if (obj != null) {
1157                 return cacheHandler(handlerName, obj);
1158             }
1159
1160             // finally walk down the this-object's parent chain
1161             if (thisObject != null) {
1162                 obj = app.getParentElement(thisObject);
1163                 // walk down parent chain to find handler object,
1164                 // limiting to 50 passes to avoid infinite loops
1165                 int maxloop = 50;
1166                 while (obj != null && maxloop-- > 0) {
1167                     Prototype proto = app.getPrototype(obj);
1168
1169                     if (proto != null && proto.isInstanceOf(handlerName)) {
1170                         return cacheHandler(handlerName, obj);
1171                     }
1172
1173                     obj = app.getParentElement(obj);
1174                 }
1175             }
1176
1177             return cacheHandler(handlerName, null);
1178         }
1179
1180         private Object cacheHandler(String name, Object handler) {
1181             if (handlerCache != null) {
1182                 handlerCache.put(name, handler);
1183             }
1184             return handler;
1185         }
1186
1187     }
1188
1189     /**
1190      * Exception type for unhandled, forbidden or failed macros
1191      */
1192     class MacroException extends Exception {
1193         MacroException(String message) {
1194             super(message);
1195         }
1196     }
1197
1198 }
1199
Note: See TracBrowser for help on using the browser.