    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}