001// Licensed under the Apache License, Version 2.0 (the "License");
002// you may not use this file except in compliance with the License.
003// You may obtain a copy of the License at
004//
005// http://www.apache.org/licenses/LICENSE-2.0
006//
007// Unless required by applicable law or agreed to in writing, software
008// distributed under the License is distributed on an "AS IS" BASIS,
009// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
010// See the License for the specific language governing permissions and
011// limitations under the License.
012
013package org.apache.tapestry5.internal.clojure;
014
015import clojure.java.api.Clojure;
016import clojure.lang.IFn;
017import clojure.lang.Symbol;
018import org.apache.tapestry5.clojure.ClojureBuilder;
019import org.apache.tapestry5.clojure.MethodToFunctionSymbolMapper;
020import org.apache.tapestry5.clojure.Namespace;
021import org.apache.tapestry5.commons.services.PlasticProxyFactory;
022import org.apache.tapestry5.commons.util.ExceptionUtils;
023import org.apache.tapestry5.ioc.OperationTracker;
024import org.apache.tapestry5.ioc.services.Builtin;
025import org.apache.tapestry5.plastic.*;
026
027import java.lang.reflect.Method;
028
029public class ClojureBuilderImpl implements ClojureBuilder
030{
031    private final PlasticProxyFactory proxyFactory;
032
033    private final MethodToFunctionSymbolMapper mapper;
034
035    private final OperationTracker tracker;
036
037    private final IFn REQUIRE = Clojure.var("clojure.core", "require");
038
039    public ClojureBuilderImpl(@Builtin PlasticProxyFactory proxyFactory, MethodToFunctionSymbolMapper mapper, OperationTracker tracker)
040    {
041        this.proxyFactory = proxyFactory;
042        this.mapper = mapper;
043        this.tracker = tracker;
044    }
045
046    @Override
047    public <T> T build(final Class<T> interfaceType)
048    {
049        assert interfaceType != null;
050        assert interfaceType.isInterface();
051
052        Namespace annotation = interfaceType.getAnnotation(Namespace.class);
053
054        if (annotation == null)
055        {
056            throw new IllegalArgumentException(String.format("Interface type %s does not have the Namespace annotation.",
057                    interfaceType.getName()));
058        }
059
060        final String namespace = annotation.value();
061
062        ClassInstantiator<T> instantiator = proxyFactory.createProxy(interfaceType, new PlasticClassTransformer()
063        {
064            @Override
065            public void transform(PlasticClass plasticClass)
066            {
067                for (final Method m : interfaceType.getMethods())
068                {
069                    bridgeToClojure(plasticClass, m);
070                }
071            }
072
073            private void bridgeToClojure(final PlasticClass plasticClass, final Method method)
074            {
075                final MethodDescription desc = new MethodDescription(method);
076
077                if (method.getReturnType() == void.class)
078                {
079                    throw new IllegalArgumentException(String.format("Method %s may not be void when bridging to Clojure functions.",
080                            desc));
081                }
082
083                final Symbol symbol = mapper.mapMethod(namespace, method);
084
085                tracker.run(String.format("Mapping %s method %s to Clojure function %s",
086                        interfaceType.getName(),
087                        desc.toShortString(),
088                        symbol.toString()), new Runnable()
089                {
090                    @Override
091                    public void run()
092                    {
093
094                        Symbol namespaceSymbol = Symbol.create(symbol.getNamespace());
095
096                        REQUIRE.invoke(namespaceSymbol);
097
098                        IFn clojureFunction = Clojure.var(symbol);
099
100                        final PlasticField fnField = plasticClass.introduceField(IFn.class, method.getName() + "IFn").inject(clojureFunction);
101
102                        plasticClass.introduceMethod(desc).changeImplementation(new InstructionBuilderCallback()
103                        {
104                            @Override
105                            public void doBuild(InstructionBuilder builder)
106                            {
107                                bridgeToClojure(builder, desc, fnField);
108                            }
109                        });
110
111                    }
112                });
113
114            }
115
116            private void bridgeToClojure(InstructionBuilder builder, MethodDescription description, PlasticField ifnField)
117            {
118                builder.loadThis().getField(ifnField);
119
120                int count = description.argumentTypes.length;
121
122                Class[] invokeParameterTypes = new Class[count];
123
124                for (int i = 0; i < count; i++)
125                {
126                    invokeParameterTypes[i] = Object.class;
127
128                    builder.loadArgument(i).boxPrimitive(description.argumentTypes[i]);
129                }
130
131                Method ifnMethod = null;
132
133                try
134                {
135                    ifnMethod = IFn.class.getMethod("invoke", invokeParameterTypes);
136                } catch (NoSuchMethodException ex)
137                {
138                    throw new RuntimeException(String.format("Unable to find correct IFn.invoke() method: %s",
139                            ExceptionUtils.toMessage(ex)), ex);
140                }
141
142                builder.invoke(ifnMethod);
143
144                builder.castOrUnbox(description.returnType);
145                builder.returnResult();
146            }
147        });
148
149        return instantiator.newInstance();
150    }
151}