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

Revision 9828, 41.6 kB (checked in by hannes, 3 weeks ago)

New logic for skin handler lookup to fix the fix for bug 617. New algorithm works like this:

  • resolve against the this-object prototype name, including extended prototypes
  • resolve against the res.handlers collection
  • resolve against the parent path of the this-object, including extended prototypes.

The following thread provides more context:
http://groups.google.com/group/helma/browse_frm/thread/b15805fd6f661d64

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