001/*
002 * Copyright (c) 2004-2010, Kohsuke Kawaguchi
003 * All rights reserved.
004 *
005 * Redistribution and use in source and binary forms, with or without modification, are permitted provided
006 * that the following conditions are met:
007 *
008 *     * Redistributions of source code must retain the above copyright notice, this list of
009 *       conditions and the following disclaimer.
010 *     * Redistributions in binary form must reproduce the above copyright notice, this list of
011 *       conditions and the following disclaimer in the documentation and/or other materials
012 *       provided with the distribution.
013 *
014 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS
015 * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
016 * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS
017 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
018 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
019 * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
020 * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
021 * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
022 */
023
024package org.kohsuke.stapler;
025
026import net.sf.json.JSONArray;
027import org.apache.commons.io.IOUtils;
028import org.kohsuke.stapler.bind.JavaScriptMethod;
029import org.kohsuke.stapler.lang.FieldRef;
030import org.kohsuke.stapler.lang.Klass;
031import org.kohsuke.stapler.lang.MethodRef;
032
033import javax.annotation.PostConstruct;
034import javax.servlet.ServletException;
035import java.io.IOException;
036import java.lang.reflect.InvocationTargetException;
037import java.lang.reflect.Type;
038import java.util.ArrayList;
039import java.util.Arrays;
040import java.util.List;
041import java.util.logging.Level;
042import java.util.logging.Logger;
043
044import static javax.servlet.http.HttpServletResponse.*;
045
046/**
047 * Created one instance each for a {@link Klass},
048 * that retains some useful cache about a class and its views.
049 *
050 * @author Kohsuke Kawaguchi
051 * @see WebApp#getMetaClass(Klass)
052 */
053public class MetaClass extends TearOffSupport {
054    private static final Logger LOGGER = Logger.getLogger(MetaClass.class.getName());
055    
056    /**
057     * This meta class wraps this class
058     *
059     * @deprecated as of 1.177
060     *      Use {@link #klass}. If you really want the Java class representation, use {@code klass.toJavaClass()}.
061     */
062    public final Class clazz;
063
064    public final Klass<?> klass;
065
066    /**
067     * {@link MetaClassLoader} that wraps {@code clazz.getClassLoader()}.
068     * Null if the class is loaded by the bootstrap classloader.
069     */
070    public final MetaClassLoader classLoader;
071
072    public final List<Dispatcher> dispatchers = new ArrayList<>();
073
074    /**
075     * Base metaclass.
076     * Note that <tt>baseClass.clazz==clazz.getSuperClass()</tt>
077     */
078    public final MetaClass baseClass;
079
080    /**
081     * {@link WebApp} that owns this meta class.
082     */
083    public final WebApp webApp;
084
085    /**
086     * If there's a method annotated with @PostConstruct, that {@link MethodRef} object, linked
087     * to the list of the base class.
088     */
089    private volatile SingleLinkedList<MethodRef> postConstructMethods;
090
091    /*package*/ MetaClass(WebApp webApp, Klass<?> klass) {
092        this.clazz = klass.toJavaClass();
093        this.klass = klass;
094        this.webApp = webApp;
095        this.baseClass = webApp.getMetaClass(klass.getSuperClass());
096        this.classLoader = MetaClassLoader.get(clazz.getClassLoader());
097        buildDispatchers();
098    }
099
100    /**
101     * Build {@link #dispatchers}.
102     *
103     * <p>
104     * This is the meat of URL dispatching. It looks at the class
105     * via reflection and figures out what URLs are handled by who.
106     */
107    /*package*/ void buildDispatchers() {
108        this.dispatchers.clear();
109        KlassDescriptor<?> node = new KlassDescriptor(klass);
110
111        dispatchers.add(new DirectoryishDispatcher());
112
113        if (HttpDeletable.class.isAssignableFrom(clazz))
114            dispatchers.add(new HttpDeletableDispatcher());
115
116        // check action <obj>.do<token>(...) and other WebMethods
117        registerDoToken(node);
118
119        // check action <obj>.doIndex(...)
120        for (Function f : node.methods.name("doIndex")) {
121            dispatchers.add(new IndexDispatcher(f.contextualize(new WebMethodContext(""))));
122        }
123
124        // JavaScript proxy method invocations for <obj>js<token>
125        // reacts only to a specific content type
126        for (Function f : node.methods.prefix("js") ) {
127            String name = camelize(f.getName().substring(2)); // jsXyz -> xyz
128            f = f.contextualize(new JavaScriptMethodContext(name));
129            dispatchers.add(new JavaScriptProxyMethodDispatcher(name, f));
130        }
131
132        // JavaScript proxy method with @JavaScriptMethod
133        // reacts only to a specific content type
134        for( final Function f : node.methods.annotated(JavaScriptMethod.class) ) {
135            JavaScriptMethod a = f.getAnnotation(JavaScriptMethod.class);
136
137            String[] names;
138            if(a!=null && a.name().length>0)   names=a.name();
139            else    names=new String[]{f.getName()};
140
141            for (String name : names)
142                dispatchers.add(new JavaScriptProxyMethodDispatcher(name,f.contextualize(new JavaScriptMethodContext(name))));
143        }
144
145        for (Facet f : webApp.facets)
146            f.buildViewDispatchers(this, dispatchers);
147
148        for (Facet f : webApp.facets)
149            f.buildIndexDispatchers(this, dispatchers);
150
151        Dispatcher d = IndexHtmlDispatcher.make(webApp.context, clazz);
152        if (d!=null)
153            dispatchers.add(d);
154
155        // check public properties of the form NODE.TOKEN
156        for (final FieldRef f : node.fields) {
157            final boolean accepted = webApp.getFilterForFields().keep(f);
158
159            dispatchers.add(new NameBasedDispatcher(f.getName()) {
160                final String role = getProtectedRole(f);
161                public boolean doDispatch(RequestImpl req, ResponseImpl rsp, Object node) throws IOException, ServletException, IllegalAccessException {
162                    if (accepted) {
163                        if (role != null && !req.isUserInRole(role))
164                            throw new IllegalAccessException("Needs to be in role " + role);
165
166                        Dispatcher.anonymizedTraceEval(req, rsp, node, "%s#%s", f.getName());
167                        if (traceable())
168                            traceEval(req, rsp, node, f.getName());
169                        req.getStapler().invoke(req, rsp, f.get(node));
170                        return true;
171                    } else {
172                        return webApp.getFilteredFieldTriggerListener().onFieldTrigger(f, req, rsp, node, f.getQualifiedName());
173                    }
174                }
175                public String toString() {
176                    if (accepted) {
177                        return String.format("%3$s %1$s for url=/%2$s/...", f.getQualifiedName(), f.getName(), f.getReturnType());
178                    } else {
179                        return String.format("BLOCKED: %3$s %1$s for url=/%2$s/...", f.getQualifiedName(), f.getName(), f.getReturnType());
180                    }
181                }
182            });
183        }
184
185        FunctionList getMethods = node.methods.prefix("get").filter(m -> !m.getSignature().equals("method java.lang.Object getClass"));
186        FunctionList filteredGetMethods;
187        if(LEGACY_GETTER_MODE || webApp.getFilterForGetMethods() == null){
188            LOGGER.log(Level.FINE, "Stapler is using the legacy GETTER_MODE");
189            filteredGetMethods = getMethods;
190        }else{
191            filteredGetMethods = getMethods.filter(webApp.getFilterForGetMethods());
192            if(LOGGER.isLoggable(Level.FINER)){
193                // to ease the debug
194                List<Function> excludedByNew = minus(getMethods, filteredGetMethods);
195    
196                if(!excludedByNew.isEmpty()){
197                    for (Function excluded : excludedByNew) {
198                        LOGGER.log(Level.FINER, "The following method is now blocked: {0}", excluded.getDisplayName());
199                    }
200                }
201            }
202        }
203
204        // check public selector methods of the form NODE.getTOKEN()
205        for (final Function f : getMethods.signature()) {
206            if(f.getName().length()<=3)
207                continue;
208
209            String name = camelize(f.getName().substring(3)); // 'getFoo' -> 'foo'
210            final Function ff = f.contextualize(new TraversalMethodContext(name));
211            final boolean isAccepted = filteredGetMethods.contains(f);
212
213            dispatchers.add(new NameBasedDispatcher(name) {
214                public boolean doDispatch(RequestImpl req, ResponseImpl rsp, Object node) throws IOException, ServletException, IllegalAccessException, InvocationTargetException {
215                    if(isAccepted){
216                        Dispatcher.anonymizedTraceEval(req, rsp, node, "%s#%s()", ff.getName());
217                        if(traceable())
218                            traceEval(req,rsp,node,ff.getName()+"()");
219                        req.getStapler().invoke(req,rsp, ff.invoke(req, rsp, node));
220                        return true;
221                    }else{
222                        return webApp.getFilteredGetterTriggerListener().onGetterTrigger(f, req, rsp, node, ff.getName()+"()");
223                    }
224                }
225                public String toString() {
226                    if(isAccepted){
227                        return String.format("%3$s %1$s() for url=/%2$s/...",ff.getQualifiedName(),name, ff.getReturnType().getName());
228                    }else{
229                        return String.format("BLOCKED: %3$s %1$s() for url=/%2$s/...",ff.getQualifiedName(),name, ff.getReturnType().getName());
230                    }
231                }
232            });
233        }
234
235        // check public selector methods of the form static NODE.getTOKEN(StaplerRequest)
236        for (final Function f : getMethods.signature(StaplerRequest.class)) {
237            if(f.getName().length()<=3)
238                continue;
239            String name = camelize(f.getName().substring(3)); // 'getFoo' -> 'foo'
240            final Function ff = f.contextualize(new TraversalMethodContext(name));
241            final boolean isAccepted = filteredGetMethods.contains(f);
242
243            dispatchers.add(new NameBasedDispatcher(name) {
244                public boolean doDispatch(RequestImpl req, ResponseImpl rsp, Object node) throws IOException, ServletException, IllegalAccessException, InvocationTargetException {
245                    if(isAccepted){
246                        Dispatcher.anonymizedTraceEval(req, rsp, node, "%s#%s(...)", ff.getName());
247                        if(traceable())
248                            traceEval(req,rsp,node,ff.getName()+"(...)");
249                        req.getStapler().invoke(req,rsp, ff.invoke(req, rsp, node, req));
250                        return true;
251                    }else{
252                        return webApp.getFilteredGetterTriggerListener().onGetterTrigger(f, req, rsp, node, ff.getName()+"(...)");
253                    }
254                }
255                public String toString() {
256                    if(isAccepted) {
257                        return String.format("%3$s %1$s(StaplerRequest) for url=/%2$s/...", ff.getQualifiedName(), name, ff.getReturnType().getName());
258                    }else{
259                        return String.format("BLOCKED: %3$s %1$s(StaplerRequest) for url=/%2$s/...", ff.getQualifiedName(), name, ff.getReturnType().getName());
260                    }
261                }
262            });
263        }
264
265        // check public selector methods <obj>.get<Token>(String)
266        for (final Function f : getMethods.signature(String.class)) {
267            if(f.getName().length()<=3)
268                continue;
269            String name = camelize(f.getName().substring(3)); // 'getFoo' -> 'foo'
270            final Function ff = f.contextualize(new TraversalMethodContext(name));
271            final boolean isAccepted = filteredGetMethods.contains(f);
272
273            dispatchers.add(new NameBasedDispatcher(name,1) {
274                public boolean doDispatch(RequestImpl req, ResponseImpl rsp, Object node) throws IOException, ServletException, IllegalAccessException, InvocationTargetException {
275                    if(isAccepted){
276                        String token = req.tokens.next();
277                        Dispatcher.anonymizedTraceEval(req, rsp, node, "%s#%s(String)", ff.getName());
278                        if(traceable())
279                            traceEval(req,rsp,node,ff.getName()+"(\""+token+"\")");
280                        req.getStapler().invoke(req,rsp, ff.invoke(req, rsp, node,token));
281                        return true;
282                    }else{
283                        String token = req.tokens.next();
284                        try{
285                            return webApp.getFilteredGetterTriggerListener().onGetterTrigger(f, req, rsp, node, ff.getName()+"(\""+token+"\")");
286                        }
287                        finally{
288                            req.tokens.prev();
289                        }
290                    }
291                }
292                public String toString() {
293                    if(isAccepted) {
294                        return String.format("%3$s %1$s(String) for url=/%2$s/TOKEN/...", ff.getQualifiedName(), name, ff.getReturnType().getName());
295                    }else{
296                        return String.format("BLOCKED: %3$s %1$s(String) for url=/%2$s/TOKEN/...", ff.getQualifiedName(), name, ff.getReturnType().getName());
297                    }
298                }
299            });
300        }
301
302        // check public selector methods <obj>.get<Token>(int)
303        for (final Function f : getMethods.signature(int.class)) {
304            if(f.getName().length()<=3)
305                continue;
306            String name = camelize(f.getName().substring(3)); // 'getFoo' -> 'foo'
307            final Function ff = f.contextualize(new TraversalMethodContext(name));
308            final boolean isAccepted = filteredGetMethods.contains(f);
309
310            dispatchers.add(new NameBasedDispatcher(name,1) {
311                public boolean doDispatch(RequestImpl req, ResponseImpl rsp, Object node) throws IOException, ServletException, IllegalAccessException, InvocationTargetException {
312                    if(isAccepted){
313                        int idx = req.tokens.nextAsInt();
314                        Dispatcher.anonymizedTraceEval(req, rsp, node, "%s#%s(int)", ff.getName());
315                        if(traceable())
316                            traceEval(req,rsp,node,ff.getName()+"("+idx+")");
317                        req.getStapler().invoke(req,rsp, ff.invoke(req, rsp, node,idx));
318                        return true;
319                    }else{
320                        int idx = req.tokens.nextAsInt();
321                        try{
322                            return webApp.getFilteredGetterTriggerListener().onGetterTrigger(f, req, rsp, node, ff.getName()+"("+idx+")");
323                        }
324                        finally{
325                            req.tokens.prev();
326                        }
327                    }
328                }
329                public String toString() {
330                    if(isAccepted){
331                        return String.format("%3$s %1$s(int) for url=/%2$s/N/...",ff.getQualifiedName(),name, ff.getReturnType().getName());
332                    }else{
333                        return String.format("BLOCKED: %3$s %1$s(int) for url=/%2$s/N/...",ff.getQualifiedName(),name, ff.getReturnType().getName());
334                    }
335                }
336            });
337        }
338
339        // check public selector methods <obj>.get<Token>(long)
340        // TF: I'm sure these for loop blocks could be dried out in some way.
341        for (final Function f : getMethods.signature(long.class)) {
342            if(f.getName().length()<=3)
343                continue;
344            String name = camelize(f.getName().substring(3)); // 'getFoo' -> 'foo'
345            final Function ff = f.contextualize(new TraversalMethodContext(name));
346            final boolean isAccepted = filteredGetMethods.contains(f);
347
348            dispatchers.add(new NameBasedDispatcher(name,1) {
349                public boolean doDispatch(RequestImpl req, ResponseImpl rsp, Object node) throws IOException, ServletException, IllegalAccessException, InvocationTargetException {
350                    if(isAccepted){
351                        long idx = req.tokens.nextAsLong();
352                        Dispatcher.anonymizedTraceEval(req, rsp, node, "%s#%s(long)", ff.getName());
353                        if(traceable())
354                            traceEval(req,rsp,node,ff.getName()+"("+idx+")");
355                        req.getStapler().invoke(req,rsp, ff.invoke(req, rsp, node,idx));
356                        return true;
357                    }else{
358                        long idx = req.tokens.nextAsLong();
359                        try{
360                            return webApp.getFilteredGetterTriggerListener().onGetterTrigger(f, req, rsp, node, ff.getName()+"("+idx+")");
361                        }
362                        finally{
363                            req.tokens.prev();
364                        }
365                    }
366                }
367                public String toString() {
368                    if(isAccepted) {
369                        return String.format("%3$s %1$s(long) for url=/%2$s/N/...", ff.getQualifiedName(), name, ff.getReturnType().getName());
370                    }else{
371                        return String.format("BLOCKED: %3$s %1$s(long) for url=/%2$s/N/...", ff.getQualifiedName(), name, ff.getReturnType().getName());
372                    }
373                }
374            });
375        }
376
377        if (klass.isArray()) {
378            dispatchers.add(new Dispatcher() {
379                public boolean dispatch(RequestImpl req, ResponseImpl rsp, Object node) throws IOException, ServletException {
380                    if(!req.tokens.hasMore())
381                        return false;
382                    try {
383                        int index = req.tokens.nextAsInt();
384                        Dispatcher.anonymizedTraceEval(req, rsp, node, "%s[idx]");
385                        if (traceable())
386                            traceEval(req, rsp, node, "", "[" + index + "]");
387                        req.getStapler().invoke(req, rsp, klass.getArrayElement(node, index));
388                        return true;
389                    } catch (IndexOutOfBoundsException e) {
390                        if(traceable())
391                            trace(req,rsp,"-> IndexOutOfRange");
392                        rsp.sendError(SC_NOT_FOUND);
393                        return true;
394                    } catch (NumberFormatException e) {
395                        return false; // try next
396                    }
397                }
398                public String toString() {
399                    return "Array look-up for url=/N/...";
400                }
401            });
402        }
403
404        if(klass.isMap()) {
405            dispatchers.add(new Dispatcher() {
406                public boolean dispatch(RequestImpl req, ResponseImpl rsp, Object node) throws IOException, ServletException {
407                    if(!req.tokens.hasMore())
408                        return false;
409                    try {
410                        String key = req.tokens.peek();
411                        Dispatcher.anonymizedTraceEval(req, rsp, node, "%s: Map access");
412                        if(traceable())
413                            traceEval(req,rsp,"",".get(\""+key+"\")");
414
415                        Object item = klass.getMapElement(node,key);
416                        if(item!=null) {
417                            req.tokens.next();
418                            req.getStapler().invoke(req,rsp,item);
419                            return true;
420                        } else {
421                            // otherwise just fall through
422                            if(traceable())
423                                trace(req,rsp,"Map.get(\""+key+"\")==null. Back tracking.");
424                            return false;
425                        }
426                    } catch (NumberFormatException e) {
427                        return false; // try next
428                    }
429                }
430                public String toString() {
431                    return "Map.get(String) look-up for url=/TOKEN/...";
432                }
433            });
434        }
435
436        // TODO: check if we can route to static resources
437        // which directory shall we look up a resource from?
438
439        for (Facet f : webApp.facets)
440            f.buildFallbackDispatchers(this, dispatchers);
441
442        // check public selector methods <obj>.getDynamic(<token>,...)
443        for (Function f : getMethods.signatureStartsWith(String.class).name("getDynamic")) {
444            final Function ff = f.contextualize(new TraversalMethodContext(TraversalMethodContext.DYNAMIC));
445            dispatchers.add(new Dispatcher() {
446                public boolean dispatch(RequestImpl req, ResponseImpl rsp, Object node) throws IllegalAccessException, InvocationTargetException, IOException, ServletException {
447                    if(!req.tokens.hasMore())
448                        return false;
449                    String token = req.tokens.next();
450                    Dispatcher.anonymizedTraceEval(req, rsp, node, "%s#getDynamic(...)");
451                    if(traceable())
452                        traceEval(req,rsp,node,"getDynamic(\""+token+"\",...)");
453
454                    Object target = ff.bindAndInvoke(node, req,rsp, token);
455                    if(target!=null) {
456                        req.getStapler().invoke(req,rsp, target);
457                        return true;
458                    } else {
459                        if(traceable())
460                            // indent:    "-> evaluate(
461                            trace(req,rsp,"            %s.getDynamic(\"%s\",...)==null. Back tracking.",node,token);
462                        req.tokens.prev(); // cancel the next effect
463                        return false;
464                    }
465                }
466                public String toString() {
467                    return String.format("%2$s %s(String,StaplerRequest,StaplerResponse) for url=/TOKEN/...",ff.getQualifiedName(), ff.getReturnType().getName());
468                }
469            });
470        }
471
472        // check action <obj>.doDynamic(...)
473        for (Function f : node.methods.name("doDynamic")) {
474            final Function ff = f.contextualize(new WebMethodContext(WebMethodContext.DYNAMIC));
475            dispatchers.add(new Dispatcher() {
476                public boolean dispatch(RequestImpl req, ResponseImpl rsp, Object node) throws IllegalAccessException, InvocationTargetException, ServletException, IOException {
477                    Dispatcher.anonymizedTraceEval(req, rsp, node, "%s#doDynamic(...)");
478                    if(traceable())
479                        trace(req,rsp,"-> <%s>.doDynamic(...)",node);
480                    return ff.bindAndInvokeAndServeResponse(node,req,rsp);
481                }
482                public String toString() {
483                    return String.format("%s(StaplerRequest,StaplerResponse) for any URL",ff.getQualifiedName());
484                }
485            });
486        }
487
488        // provide a "last chance" for the application to add/remove dispatchers
489        DispatchersFilter dispatchersFilter = webApp.getDispatchersFilter();
490        if(dispatchersFilter != null){
491            dispatchersFilter.applyOn(this, node.methods, dispatchers);
492        }
493    }
494
495    private void registerDoToken(KlassDescriptor<?> node){
496        final FunctionList filteredFunctions;
497        FunctionList functions;
498        if(LEGACY_WEB_METHOD_MODE || webApp.getFilterForDoActions() == null){
499            functions = node.methods.webMethodsLegacy();
500            filteredFunctions = functions;
501            LOGGER.log(Level.FINE, "Stapler is using the legacy METHOD_MODE");
502        } else {
503            functions = node.methods.webMethodsLegacy();
504            filteredFunctions = functions.filter(webApp.getFilterForDoActions());
505            if(LOGGER.isLoggable(Level.FINER)){
506                List<Function> excludedByNew = minus(functions, filteredFunctions);
507                
508                if(!excludedByNew.isEmpty()){
509                    for (Function excluded : excludedByNew) {
510                        LOGGER.log(Level.FINER, "The following method is now blocked: {0}", excluded.getDisplayName());
511                    }
512                }
513            }
514        }
515        
516        for (final Function f : functions) {
517            WebMethod a = f.getAnnotation(WebMethod.class);
518            
519            String[] names;
520            if(a!=null && a.name().length>0)   names=a.name();
521            else    names=new String[]{camelize(f.getName().substring(2))}; // 'doFoo' -> 'foo'
522            
523            for (String name : names) {
524                final Function ff = f.contextualize(new WebMethodContext(name));
525                if (name.length()==0) {
526                    dispatchers.add(new IndexDispatcher(ff));
527                } else {
528                    final boolean isAccepted = filteredFunctions.contains(f);
529                    dispatchers.add(new NameBasedDispatcher(name) {
530                        public boolean doDispatch(RequestImpl req, ResponseImpl rsp, Object node) throws IllegalAccessException, InvocationTargetException, ServletException, IOException {
531                            if(isAccepted){
532                                Dispatcher.anonymizedTraceEval(req, rsp, node, "%s#%s", ff.getName());
533                                if (traceable())
534                                    trace(req, rsp, "-> <%s>.%s(...)", node, ff.getName());
535                                return ff.bindAndInvokeAndServeResponse(node, req, rsp);
536                            }else{
537                                return webApp.getFilteredDoActionTriggerListener().onDoActionTrigger(f, req, rsp, node);
538                            }
539                        }
540                        
541                        public String toString() {
542                            if(isAccepted){
543                                return String.format("%1$s(...) for url=/%2$s/...", ff.getQualifiedName(), name);
544                            }else{
545                                return String.format("BLOCKED: %1$s(...) for url=/%2$s/...", ff.getQualifiedName(), name);
546                            }
547                        }
548                    });
549                }
550            }
551        }
552    }
553    
554    /**
555     * Return (A - B)
556     */
557    private List<Function> minus(FunctionList a, FunctionList b){
558        List<Function> aMinusB = new ArrayList<>();
559        for(Function f : a){
560            if(!b.contains(f)){
561                aMinusB.add(f);
562            }
563        }
564    
565        return aMinusB;
566    }
567    
568    /**
569     * Returns all the methods in the ancestry chain annotated with {@link PostConstruct}
570     * from those defined in the derived type toward those defined in the base type.
571     *
572     * Normally invocation requires visiting the list in the reverse order.
573     * @since 1.220
574     */
575    public SingleLinkedList<MethodRef> getPostConstructMethods() {
576        if (postConstructMethods ==null) {
577            SingleLinkedList<MethodRef> l = baseClass==null ? SingleLinkedList.<MethodRef>empty() : baseClass.getPostConstructMethods();
578
579            for (MethodRef mr : klass.getDeclaredMethods()) {
580                if (mr.hasAnnotation(PostConstruct.class)) {
581                    l = l.grow(mr);
582                }
583            }
584            postConstructMethods = l;
585        }
586        return postConstructMethods;
587    }
588
589    private String getProtectedRole(FieldRef f) {
590        try {
591            LimitedTo a = f.getAnnotation(LimitedTo.class);
592            return (a!=null)?a.value():null;
593        } catch (LinkageError e) {
594            return null;    // running in JDK 1.4
595        }
596    }
597
598    @Override
599    public String toString() {
600        return "MetaClass["+klass+"]";
601    }
602
603    private static String camelize(String name) {
604        return Character.toLowerCase(name.charAt(0))+name.substring(1);
605    }
606
607    private static class JavaScriptProxyMethodDispatcher extends NameBasedDispatcher {
608        private final Function f;
609
610        public JavaScriptProxyMethodDispatcher(String name, Function f) {
611            super(name, 0);
612            this.f = f;
613        }
614
615        public boolean doDispatch(RequestImpl req, ResponseImpl rsp, Object node) throws IllegalAccessException, InvocationTargetException, ServletException, IOException {
616            if (!req.isJavaScriptProxyCall())
617                return false;
618
619            req.stapler.getWebApp().getCrumbIssuer().validateCrumb(req,req.getHeader("Crumb"));
620
621            Dispatcher.anonymizedTraceEval(req, rsp, node, "%s#%s", f.getName());
622            if(traceable())
623                trace(req,rsp,"-> <%s>.%s(...)",node, f.getName());
624
625            JSONArray jsargs = JSONArray.fromObject(IOUtils.toString(req.getReader()));
626            Object[] args = new Object[jsargs.size()];
627            Class[] types = f.getParameterTypes();
628            Type[] genericTypes = f.getGenericParameterTypes();
629            if (args.length != types.length) {
630                throw new IllegalArgumentException("argument count mismatch between " + jsargs + " and " + Arrays.toString(genericTypes));
631            }
632
633            for (int i=0; i<args.length; i++)
634                args[i] = req.bindJSON(genericTypes[i],types[i],jsargs.get(i));
635
636            return f.bindAndInvokeAndServeResponse(node,req,rsp,args);
637        }
638
639        public String toString() {
640            return f.getQualifiedName()+"(...) for url=/"+name+"/...";
641        }
642    }
643
644    /**
645     * Don't cache anything in memory, so that any change
646     * will take effect instantly.
647     */
648    public static boolean NO_CACHE = false;
649    /**
650     * In case the breaking changes are not desired. They are recommended for security reason.
651     */
652    public static boolean LEGACY_GETTER_MODE = false;
653    /**
654     * In case the breaking changes are not desired. They are recommended for security reason.
655     */
656    public static boolean LEGACY_WEB_METHOD_MODE = false;
657
658    static {
659        try {
660            NO_CACHE = Boolean.getBoolean("stapler.jelly.noCache");
661            LEGACY_GETTER_MODE = Boolean.getBoolean("stapler.legacyGetterDispatcherMode");
662            LEGACY_WEB_METHOD_MODE = Boolean.getBoolean("stapler.legacyWebMethodDispatcherMode");
663        } catch (SecurityException e) {
664            // ignore.
665        }
666    }
667}