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