001    /*****************************************************************************
002     * Copyright (C) PicoContainer Organization. All rights reserved.            *
003     * ------------------------------------------------------------------------- *
004     * The software in this package is published under the terms of the BSD      *
005     * style license a copy of which has been included with this distribution in *
006     * the LICENSE.txt file.                                                     *
007     *                                                                           *
008     * Original code by Leo Simons                                               *
009     *****************************************************************************/
010    package org.picocontainer.script.bsh;
011    
012    import bsh.EvalError;
013    import bsh.Interpreter;
014    import org.picocontainer.Parameter;
015    import org.picocontainer.PicoContainer;
016    import org.picocontainer.PicoCompositionException;
017    import org.picocontainer.adapters.AbstractAdapter;
018    
019    import java.io.IOException;
020    import java.io.InputStreamReader;
021    import java.io.Reader;
022    import java.net.URL;
023    import java.util.Arrays;
024    import java.util.Collections;
025    import java.lang.reflect.Type;
026    
027    /**
028     * This adapter relies on <a href="http://beanshell.org/">Bsh</a> for instantiation
029     * (and possibly also initialisation) of component instances.
030     * <p/>
031     * When {@link org.picocontainer.ComponentAdapter#getComponentInstance} is called (by PicoContainer),
032     * the adapter instance will look for a script with the same name as the component implementation
033     * class (but with the .bsh extension). This script must reside in the same folder as the class.
034     * (It's ok to have them both in a jar).
035     * <p/>
036     * The bsh script's only contract is that it will have to instantiate a bsh variable called
037     * "instance".
038     * <p/>
039     * The script will have access to the following variables:
040     * <ul>
041     * <li>addAdapter - the adapter calling the script</li>
042     * <li>picoContainer - the MutablePicoContainer calling the addAdapter</li>
043     * <li>componentKey - the component key</li>
044     * <li>componentImplementation - the component implementation</li>
045     * <li>parameters - the ComponentParameters (as a List)</li>
046     * </ul>
047     * @author <a href="mail at leosimons dot com">Leo Simons</a>
048     * @author Aslak Hellesoy
049     */
050    @SuppressWarnings({ "unchecked", "serial" })
051    public class BeanShellAdapter extends AbstractAdapter {
052        private final Parameter[] parameters;
053    
054        private Object instance = null;
055    
056        /**
057         * Classloader to set for the BeanShell interpreter.
058         */
059        private final ClassLoader classLoader;
060    
061        public BeanShellAdapter(final Object componentKey, final Class<?> componentImplementation, final Parameter[] parameters, final ClassLoader classLoader) {
062            super(componentKey, componentImplementation);
063            this.parameters = parameters;
064            this.classLoader = classLoader;
065        }
066    
067        public BeanShellAdapter(final Object componentKey, final Class<?> componentImplementation, final Parameter... parameters) {
068            this(componentKey, componentImplementation, parameters, BeanShellAdapter.class.getClassLoader());
069        }
070    
071        public Object getComponentInstance(PicoContainer pico, Type into)
072                throws PicoCompositionException
073        {
074    
075            if (instance == null) {
076                try {
077                    Interpreter i = new Interpreter();
078                    i.setClassLoader(classLoader);
079                    i.set("addAdapter", this);
080                    i.set("picoContainer", pico);
081                    i.set("componentKey", getComponentKey());
082                    i.set("componentImplementation", getComponentImplementation());
083                    i.set("parameters", parameters != null ? Arrays.asList(parameters) : Collections.EMPTY_LIST);
084                    i.eval("import " + getComponentImplementation().getName() + ";");
085    
086                    String scriptPath = "/" + getComponentImplementation().getName().replace('.', '/') + ".bsh";
087    
088                    // Inside IDEA, this relies on the compilation output path being the same directory as the source path.
089                    // kludge - copy ScriptableDemoBean.bsh to the same location in the test output compile class path.
090                    // the same problem exists for maven, but some custom jelly script will be able to fix that.
091                    URL scriptURL = getComponentImplementation().getResource(scriptPath);
092                    if (scriptURL == null) {
093                        throw new BeanShellScriptCompositionException("Couldn't load script at path " + scriptPath);
094                    }
095                    Reader sourceReader = new InputStreamReader(scriptURL.openStream());
096                    i.eval(sourceReader, i.getNameSpace(), scriptURL.toExternalForm());
097    
098                    instance = i.get("instance");
099                    if (instance == null) {
100                        throw new BeanShellScriptCompositionException("The 'instance' variable was not instantiated");
101                    }
102                } catch (EvalError e) {
103                    throw new BeanShellScriptCompositionException(e);
104                } catch (IOException e) {
105                    throw new BeanShellScriptCompositionException(e);
106                }
107            }
108            return instance;
109        }
110    
111        public void verify(PicoContainer pico) {
112        }
113    
114        public String getDescriptor() {
115            return "BeanShellConsole";
116        }
117    }