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.ioc.internal.services;
014
015import org.apache.tapestry5.commons.services.PlasticProxyFactory;
016import org.apache.tapestry5.commons.util.CollectionFactory;
017import org.apache.tapestry5.plastic.*;
018import org.slf4j.Logger;
019
020import java.util.Iterator;
021import java.util.List;
022
023/**
024 * Used by the {@link org.apache.tapestry5.ioc.internal.services.PipelineBuilderImpl} to create bridge classes and to
025 * create instances of bridge classes. A bridge class implements the <em>service</em> interface. Within the chain,
026 * bridge 1 is passed to filter 1. Invoking methods on bridge 1 will invoke methods on filter 2.
027 */
028public class BridgeBuilder<S, F>
029{
030    private static final MethodDescription GET_PIPELINE_FILTER = new MethodDescription("java.lang.Object",
031            "getPipelineFilter");
032
033    private static final MethodDescription GET_PIPELINE_NEXT = new MethodDescription("java.lang.Object",
034            "getPipelineNext");
035
036    private final Logger logger;
037
038    private final Class<S> serviceInterface;
039
040    private final Class<F> filterInterface;
041
042    private final String header;
043
044    private final FilterMethodAnalyzer filterMethodAnalyzer;
045
046    private final PlasticProxyFactory proxyFactory;
047
048    private ClassInstantiator<S> instantiator;
049
050    public BridgeBuilder(Logger logger, Class<S> serviceInterface, Class<F> filterInterface, PlasticProxyFactory proxyFactory)
051    {
052        this.logger = logger;
053        this.serviceInterface = serviceInterface;
054        this.filterInterface = filterInterface;
055
056        this.proxyFactory = proxyFactory;
057
058        header = PipelineDescriber.header(serviceInterface, filterInterface);
059
060        filterMethodAnalyzer = new FilterMethodAnalyzer(serviceInterface);
061    }
062
063    /**
064     * Instantiates a bridge object.
065     *
066     * @param nextBridge
067     *         the next Bridge object in the pipeline, or the terminator service
068     * @param filter
069     *         the filter object for this step of the pipeline
070     */
071    public S instantiateBridge(S nextBridge, F filter)
072    {
073        if (instantiator == null)
074            createInstantiator();
075
076        return instantiator.with(filterInterface, filter).with(serviceInterface, nextBridge).newInstance();
077    }
078
079    private void createInstantiator()
080    {
081        instantiator = proxyFactory.createProxy(serviceInterface, new PlasticClassTransformer()
082        {
083            @Override
084            public void transform(PlasticClass plasticClass)
085            {
086                PlasticField filterField = plasticClass.introduceField(filterInterface, "filter")
087                        .injectFromInstanceContext();
088                PlasticField nextField = plasticClass.introduceField(serviceInterface, "next")
089                        .injectFromInstanceContext();
090
091                processMethods(plasticClass, filterField, nextField);
092
093                introducePipelineStep(plasticClass, filterField, nextField);
094            }
095        });
096    }
097
098    /**
099     * Makes the bridge a {@link PipelineStep}, exposing the filter it wraps and what it delegates to, and adds a
100     * {@code toString()} that describes the pipeline from this step onwards.
101     */
102    private void introducePipelineStep(PlasticClass plasticClass, final PlasticField filterField,
103                                       final PlasticField nextField)
104    {
105        // Rule out that a service interface declares one of the methods we want to introduce.
106        // Fall back to a plain description.
107
108        if (plasticClass.isMethodImplemented(GET_PIPELINE_FILTER) || plasticClass.isMethodImplemented(GET_PIPELINE_NEXT))
109        {
110            plasticClass.addToString(header);
111
112            return;
113        }
114
115        plasticClass.introduceInterface(PipelineStep.class);
116
117        plasticClass.introduceMethod(GET_PIPELINE_FILTER, new InstructionBuilderCallback()
118        {
119            @Override
120            public void doBuild(InstructionBuilder builder)
121            {
122                builder.loadThis().getField(filterField).returnResult();
123            }
124        });
125
126        plasticClass.introduceMethod(GET_PIPELINE_NEXT, new InstructionBuilderCallback()
127        {
128            @Override
129            public void doBuild(InstructionBuilder builder)
130            {
131                builder.loadThis().getField(nextField).returnResult();
132            }
133        });
134
135        addToString(plasticClass);
136    }
137
138    /**
139     * Adds a {@code toString()} that delegates to
140     * {@link PipelineDescriber#describe(String, PipelineStep)}, describing the whole chain from this bridge onwards.
141     *
142     * Nothing is added when {@code toString()} is part of the service interface itself; in that case the method is
143     * already bridged to the filter, and that takes precedence. The pipeline can still be walked as a
144     * {@link PipelineStep}.
145     */
146    private void addToString(PlasticClass plasticClass)
147    {
148        if (plasticClass.isMethodImplemented(PlasticUtils.TO_STRING_DESCRIPTION))
149            return;
150
151        plasticClass.introduceMethod(PlasticUtils.TO_STRING_DESCRIPTION, new InstructionBuilderCallback()
152        {
153            @Override
154            public void doBuild(InstructionBuilder builder)
155            {
156                builder.loadConstant(header).loadThis();
157
158                builder.invokeStatic(PipelineDescriber.class, String.class, "describe", String.class,
159                        PipelineStep.class);
160
161                builder.returnResult();
162            }
163        });
164    }
165
166    private void processMethods(PlasticClass plasticClass, PlasticField filterField, PlasticField nextField)
167    {
168        List<MethodSignature> serviceMethods = CollectionFactory.newList();
169        List<MethodSignature> filterMethods = CollectionFactory.newList();
170
171        MethodIterator mi = new MethodIterator(serviceInterface);
172
173        while (mi.hasNext())
174        {
175            serviceMethods.add(mi.next());
176        }
177
178        mi = new MethodIterator(filterInterface);
179
180        while (mi.hasNext())
181        {
182            filterMethods.add(mi.next());
183        }
184
185        while (!serviceMethods.isEmpty())
186        {
187            MethodSignature ms = serviceMethods.remove(0);
188
189            addBridgeMethod(plasticClass, filterField, nextField, ms, filterMethods);
190        }
191
192        reportExtraFilterMethods(filterMethods);
193    }
194
195    private void reportExtraFilterMethods(List filterMethods)
196    {
197        Iterator i = filterMethods.iterator();
198
199        while (i.hasNext())
200        {
201            MethodSignature ms = (MethodSignature) i.next();
202
203            logger.error("Method {} of filter interface {} does not have a matching method in {}.", ms, filterInterface.getName(), serviceInterface.getName());
204        }
205    }
206
207    /**
208     * Finds a matching method in filterMethods for the given service method. A matching method has the same signature
209     * as the service interface method, but with an additional parameter matching the service interface itself.
210     *
211     * The matching method signature from the list of filterMethods is removed and code generation strategies for making
212     * the two methods call each other are added.
213     */
214    private void addBridgeMethod(PlasticClass plasticClass, PlasticField filterField, PlasticField nextField,
215                                 final MethodSignature ms, List filterMethods)
216    {
217        PlasticMethod method = plasticClass.introduceMethod(ms.getMethod());
218
219        Iterator i = filterMethods.iterator();
220
221        while (i.hasNext())
222        {
223            MethodSignature fms = (MethodSignature) i.next();
224
225            int position = filterMethodAnalyzer.findServiceInterfacePosition(ms, fms);
226
227            if (position >= 0)
228            {
229                bridgeServiceMethodToFilterMethod(method, filterField, nextField, position, ms, fms);
230                i.remove();
231                return;
232            }
233        }
234
235        method.changeImplementation(new InstructionBuilderCallback()
236        {
237            @Override
238            public void doBuild(InstructionBuilder builder)
239            {
240                String message = String.format("Method %s has no match in filter interface %s.", ms, filterInterface.getName());
241
242                logger.error(message);
243
244                builder.throwException(RuntimeException.class, message);
245            }
246        });
247    }
248
249    private void bridgeServiceMethodToFilterMethod(PlasticMethod method, final PlasticField filterField,
250                                                   final PlasticField nextField, final int position, MethodSignature ms, final MethodSignature fms)
251    {
252        method.changeImplementation(new InstructionBuilderCallback()
253        {
254            @Override
255            public void doBuild(InstructionBuilder builder)
256            {
257                builder.loadThis().getField(filterField);
258
259                int argumentIndex = 0;
260
261                for (int i = 0; i < fms.getParameterTypes().length; i++)
262                {
263                    if (i == position)
264                    {
265                        builder.loadThis().getField(nextField);
266                    } else
267                    {
268                        builder.loadArgument(argumentIndex++);
269                    }
270                }
271
272                builder.invoke(fms.getMethod()).returnResult();
273            }
274        });
275    }
276
277}