    001// Copyright 2006, 2007, 2008, 2009, 2010, 2011, 2012 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.ioc.internal;
    016
    017import org.apache.tapestry5.commons.*;
    018import org.apache.tapestry5.commons.internal.util.*;
    019import org.apache.tapestry5.commons.services.PlasticProxyFactory;
    020import org.apache.tapestry5.commons.services.TypeCoercer;
    021import org.apache.tapestry5.commons.util.CollectionFactory;
    022import org.apache.tapestry5.ioc.AdvisorDef;
    023import org.apache.tapestry5.ioc.Invokable;
    024import org.apache.tapestry5.ioc.Markable;
    025import org.apache.tapestry5.ioc.OperationTracker;
    026import org.apache.tapestry5.ioc.ServiceBuilderResources;
    027import org.apache.tapestry5.ioc.ServiceLifecycle2;
    028import org.apache.tapestry5.ioc.ServiceResources;
    029import org.apache.tapestry5.ioc.annotations.Local;
    030import org.apache.tapestry5.ioc.def.*;
    031import org.apache.tapestry5.ioc.internal.services.JustInTimeObjectCreator;
    032import org.apache.tapestry5.ioc.internal.util.ConcurrentBarrier;
    033import org.apache.tapestry5.ioc.internal.util.InjectionResources;
    034import org.apache.tapestry5.ioc.internal.util.InternalUtils;
    035import org.apache.tapestry5.ioc.internal.util.MapInjectionResources;
    036import org.apache.tapestry5.ioc.services.AspectDecorator;
    037import org.apache.tapestry5.ioc.services.Status;
    038import org.apache.tapestry5.plastic.*;
    039import org.slf4j.Logger;
    040
    041import java.io.ObjectStreamException;
    042import java.io.Serializable;
    043import java.lang.reflect.Constructor;
    044import java.lang.reflect.InvocationTargetException;
    045import java.lang.reflect.Method;
    046import java.lang.reflect.Modifier;
    047import java.util.*;
    048
    049import static java.lang.String.format;
    050
    051@SuppressWarnings("all")
    052public class ModuleImpl implements Module
    053{
    054    private final InternalRegistry registry;
    055
    056    private final ServiceActivityTracker tracker;
    057
    058    private final ModuleDef2 moduleDef;
    059
    060    private final PlasticProxyFactory proxyFactory;
    061
    062    private final Logger logger;
    063
    064    /**
    065     * Lazily instantiated. Access is guarded by BARRIER.
    066     */
    067    private Object moduleInstance;
    068
    069    // Set to true when invoking the module constructor. Used to
    070    // detect endless loops caused by irresponsible dependencies in
    071    // the constructor.
    072    private boolean insideConstructor;
    073
    074    /**
    075     * Keyed on fully qualified service id; values are instantiated services (proxies). Guarded by BARRIER.
    076     */
    077    private final Map<String, Object> services = CollectionFactory.newCaseInsensitiveMap();
    078
    079    private final Map<String, ServiceDef3> serviceDefs = CollectionFactory.newCaseInsensitiveMap();
    080
    081    /**
    082     * The barrier is shared by all modules, which means that creation of *any* service for any module is single
    083     * threaded.
    084     */
    085    private final static ConcurrentBarrier BARRIER = new ConcurrentBarrier();
    086
    087    /**
    088     * "Magic" method related to Serializable that allows the Proxy object to replace itself with the token when being
    089     * streamed out.
    090     */
    091    private static final MethodDescription WRITE_REPLACE = new MethodDescription(Modifier.PRIVATE, "java.lang.Object",
    092            "writeReplace", null, null, new String[]
    093            {ObjectStreamException.class.getName()});
    094
    095    public ModuleImpl(InternalRegistry registry, ServiceActivityTracker tracker, ModuleDef moduleDef,
    096                      PlasticProxyFactory proxyFactory, Logger logger)
    097    {
    098        this.registry = registry;
    099        this.tracker = tracker;
    100        this.proxyFactory = proxyFactory;
    101        this.moduleDef = InternalUtils.toModuleDef2(moduleDef);
    102        this.logger = logger;
    103
    104        for (String id : moduleDef.getServiceIds())
    105        {
    106            ServiceDef sd = moduleDef.getServiceDef(id);
    107
    108            ServiceDef3 sd3 = InternalUtils.toServiceDef3(sd);
    109
    110            serviceDefs.put(id, sd3);
    111        }
    112    }
    113
    114    @Override
    115    public <T> T getService(String serviceId, Class<T> serviceInterface)
    116    {
    117        assert InternalUtils.isNonBlank(serviceId);
    118        assert serviceInterface != null;
    119        ServiceDef3 def = getServiceDef(serviceId);
    120
    121        // RegistryImpl should already have checked that the service exists.
    122        assert def != null;
    123
    124        Object service = findOrCreate(def, null);
    125
    126        try
    127        {
    128            return serviceInterface.cast(service);
    129        } catch (ClassCastException ex)
    130        {
    131            // This may be overkill: I don't know how this could happen
    132            // given that the return type of the method determines
    133            // the service interface.
    134
    135            throw new RuntimeException(IOCMessages.serviceWrongInterface(serviceId, def.getServiceInterface(),
    136                    serviceInterface));
    137        }
    138    }
    139
    140    @Override
    141    public Set<DecoratorDef> findMatchingDecoratorDefs(ServiceDef serviceDef)
    142    {
    143        Set<DecoratorDef> result = CollectionFactory.newSet();
    144
    145        for (DecoratorDef def : moduleDef.getDecoratorDefs())
    146        {
    147            if (def.matches(serviceDef) || markerMatched(serviceDef, InternalUtils.toDecoratorDef2(def)))
    148                result.add(def);
    149        }
    150
    151        return result;
    152    }
    153
    154    @Override
    155    public Set<AdvisorDef> findMatchingServiceAdvisors(ServiceDef serviceDef)
    156    {
    157        Set<AdvisorDef> result = CollectionFactory.newSet();
    158
    159        for (AdvisorDef def : moduleDef.getAdvisorDefs())
    160        {
    161            if (def.matches(serviceDef) || markerMatched(serviceDef, InternalUtils.toAdvisorDef2(def)))
    162                result.add(def);
    163        }
    164
    165        return result;
    166    }
    167
    168    @Override
    169    @SuppressWarnings("unchecked")
    170    public Collection<String> findServiceIdsForInterface(Class serviceInterface)
    171    {
    172        assert serviceInterface != null;
    173        Collection<String> result = CollectionFactory.newList();
    174
    175        for (ServiceDef2 def : serviceDefs.values())
    176        {
    177            if (serviceInterface.isAssignableFrom(def.getServiceInterface()))
    178                result.add(def.getServiceId());
    179        }
    180
    181        return result;
    182    }
    183
    184    /**
    185     * Locates the service proxy for a particular service (from the service definition).
    186     *
    187     * @param def              defines the service
    188     * @param eagerLoadProxies collection into which proxies for eager loaded services are added (or null)
    189     * @return the service proxy
    190     */
    191    private Object findOrCreate(final ServiceDef3 def, final Collection<EagerLoadServiceProxy> eagerLoadProxies)
    192    {
    193        final String key = def.getServiceId();
    194
    195        final Invokable create = new Invokable()
    196        {
    197            @Override
    198            public Object invoke()
    199            {
    200                // In a race condition, two threads may try to create the same service simulatenously.
    201                // The second will block until after the first creates the service.
    202
    203                Object result = services.get(key);
    204
    205                // Normally, result is null, unless some other thread slipped in and created the service
    206                // proxy.
    207
    208                if (result == null)
    209                {
    210                    result = create(def, eagerLoadProxies);
    211
    212                    services.put(key, result);
    213                }
    214
    215                return result;
    216            }
    217        };
    218
    219        Invokable find = new Invokable()
    220        {
    221            @Override
    222            public Object invoke()
    223            {
    224                Object result = services.get(key);
    225
    226                if (result == null)
    227                    result = BARRIER.withWrite(create);
    228
    229                return result;
    230            }
    231        };
    232
    233        return BARRIER.withRead(find);
    234    }
    235
    236    @Override
    237    public void collectEagerLoadServices(final Collection<EagerLoadServiceProxy> proxies)
    238    {
    239        Runnable work = new Runnable()
    240        {
    241            @Override
    242            public void run()
    243            {
    244                for (ServiceDef3 def : serviceDefs.values())
    245                {
    246                    if (def.isEagerLoad())
    247                        findOrCreate(def, proxies);
    248                }
    249            }
    250        };
    251
    252        registry.run("Eager loading services", work);
    253    }
    254
    255    /**
    256     * Creates the service and updates the cache of created services.
    257     *
    258     * @param eagerLoadProxies a list into which any eager loaded proxies should be added
    259     */
    260    private Object create(final ServiceDef3 def, final Collection<EagerLoadServiceProxy> eagerLoadProxies)
    261    {
    262        final String serviceId = def.getServiceId();
    263
    264        final Logger logger = registry.getServiceLogger(serviceId);
    265
    266        final Class serviceInterface = def.getServiceInterface();
    267
    268        String description = String.format("Creating %s service %s",
    269                serviceInterface.isInterface() ? "proxy for" : "non-proxied instance of",
    270                serviceId);
    271
    272        if (logger.isDebugEnabled())
    273            logger.debug(description);
    274
    275        final Module module = this;
    276
    277        Invokable operation = new Invokable()
    278        {
    279            @Override
    280            public Object invoke()
    281            {
    282                try
    283                {
    284                    ServiceBuilderResources resources = new ServiceResourcesImpl(registry, module, def, proxyFactory,
    285                            logger);
    286
    287                    // Build up a stack of operations that will be needed to realize the service
    288                    // (by the proxy, at a later date).
    289
    290                    ObjectCreator creator = def.createServiceCreator(resources);
    291
    292
    293                    // For non-proxyable services, we immediately create the service implementation
    294                    // and return it. There's no interface to proxy, which throws out the possibility of
    295                    // deferred instantiation, service lifecycles, and decorators.
    296
    297                    ServiceLifecycle2 lifecycle = registry.getServiceLifecycle(def.getServiceScope());
    298
    299                    if (!serviceInterface.isInterface())
    300                    {
    301                        if (lifecycle.requiresProxy())
    302                            throw new IllegalArgumentException(
    303                                    String.format(
    304                                            "Service scope '%s' requires a proxy, but the service does not have a service interface (necessary to create a proxy). Provide a service interface or select a different service scope.",
    305                                            def.getServiceScope()));
    306
    307                        return creator.createObject();
    308                    }
    309
    310                    creator = new OperationTrackingObjectCreator(registry, String.format("Instantiating service %s implementation via %s", serviceId, creator), creator);
    311
    312                    creator = new LifecycleWrappedServiceCreator(lifecycle, resources, creator);
    313
    314                    // Marked services (or services inside marked modules) are not decorated.
    315                    // TapestryIOCModule prevents decoration of its services. Note that all decorators will decorate
    316                    // around the aspect interceptor, which wraps around the core service implementation.
    317
    318                    boolean allowDecoration = !def.isPreventDecoration();
    319
    320                    if (allowDecoration)
    321                    {
    322                        creator = new AdvisorStackBuilder(def, creator, getAspectDecorator(), registry);
    323                        creator = new InterceptorStackBuilder(def, creator, registry);
    324                    }
    325
    326                    // Add a wrapper that checks for recursion.
    327
    328                    creator = new RecursiveServiceCreationCheckWrapper(def, creator, logger);
    329
    330                    creator = new OperationTrackingObjectCreator(registry, "Realizing service " + serviceId, creator);
    331
    332                    JustInTimeObjectCreator delegate = new JustInTimeObjectCreator(tracker, creator, serviceId);
    333
    334                    Object proxy = createProxy(resources, delegate, def.isPreventDecoration());
    335
    336                    registry.addRegistryShutdownListener(delegate);
    337
    338                    // Occasionally eager load service A may invoke service B from its service builder method; if
    339                    // service B is eager loaded, we'll hit this method but eagerLoadProxies will be null. That's OK
    340                    // ... service B is being realized anyway.
    341
    342                    if (def.isEagerLoad() && eagerLoadProxies != null)
    343                        eagerLoadProxies.add(delegate);
    344
    345                    tracker.setStatus(serviceId, Status.VIRTUAL);
    346
    347                    return proxy;
    348                } catch (Exception ex)
    349                {
    350                    ex.printStackTrace();
    351                    throw new RuntimeException(IOCMessages.errorBuildingService(serviceId, def, ex), ex);
    352                }
    353            }
    354        };
    355
    356        return registry.invoke(description, operation);
    357    }
    358
    359    private AspectDecorator getAspectDecorator()
    360    {
    361        return registry.invoke("Obtaining AspectDecorator service", new Invokable<AspectDecorator>()
    362        {
    363            @Override
    364            public AspectDecorator invoke()
    365            {
    366                return registry.getService(AspectDecorator.class);
    367            }
    368        });
    369    }
    370
    371    private final Runnable instantiateModule = new Runnable()
    372    {
    373        @Override
    374        public void run()
    375        {
    376            moduleInstance = registry.invoke("Constructing module class " + moduleDef.getBuilderClass().getName(),
    377                    new Invokable()
    378                    {
    379                        @Override
    380                        public Object invoke()
    381                        {
    382                            return instantiateModuleInstance();
    383                        }
    384                    });
    385        }
    386    };
    387
    388    private final Invokable provideModuleInstance = new Invokable<Object>()
    389    {
    390        @Override
    391        public Object invoke()
    392        {
    393            if (moduleInstance == null)
    394                BARRIER.withWrite(instantiateModule);
    395
    396            return moduleInstance;
    397        }
    398    };
    399
    400    @Override
    401    public Object getModuleBuilder()
    402    {
    403        return BARRIER.withRead(provideModuleInstance);
    404    }
    405
    406    private Object instantiateModuleInstance()
    407    {
    408        Class moduleClass = moduleDef.getBuilderClass();
    409
    410        Constructor[] constructors = moduleClass.getConstructors();
    411
    412        if (constructors.length == 0)
    413            throw new RuntimeException(IOCMessages.noPublicConstructors(moduleClass));
    414
    415        if (constructors.length > 1)
    416        {
    417            // Sort the constructors ascending by number of parameters (descending); this is really
    418            // just to allow the test suite to work properly across different JVMs (which will
    419            // often order the constructors differently).
    420
    421            Comparator<Constructor> comparator = new Comparator<Constructor>()
    422            {
    423                @Override
    424                public int compare(Constructor c1, Constructor c2)
    425                {
    426                    return c2.getParameterTypes().length - c1.getParameterTypes().length;
    427                }
    428            };
    429
    430            Arrays.sort(constructors, comparator);
    431
    432            logger.warn(IOCMessages.tooManyPublicConstructors(moduleClass, constructors[0]));
    433        }
    434
    435        Constructor constructor = constructors[0];
    436
    437        if (insideConstructor)
    438            throw new RuntimeException(IOCMessages.recursiveModuleConstructor(moduleClass, constructor));
    439
    440        ObjectLocator locator = new ObjectLocatorImpl(registry, this);
    441        Map<Class, Object> resourcesMap = CollectionFactory.newMap();
    442
    443        resourcesMap.put(Logger.class, logger);
    444        resourcesMap.put(ObjectLocator.class, locator);
    445        resourcesMap.put(OperationTracker.class, registry);
    446
    447        InjectionResources resources = new MapInjectionResources(resourcesMap);
    448
    449        Throwable fail = null;
    450
    451        try
    452        {
    453            insideConstructor = true;
    454
    455            ObjectCreator[] parameterValues = InternalUtils.calculateParameters(locator, resources,
    456                    constructor.getParameterTypes(), constructor.getGenericParameterTypes(),
    457                    constructor.getParameterAnnotations(), registry);
    458
    459            Object[] realized = InternalUtils.realizeObjects(parameterValues);
    460
    461            Object result = constructor.newInstance(realized);
    462
    463            InternalUtils.injectIntoFields(result, locator, resources, registry);
    464
    465            return result;
    466        } catch (InvocationTargetException ex)
    467        {
    468            fail = ex.getTargetException();
    469        } catch (Exception ex)
    470        {
    471            fail = ex;
    472        } finally
    473        {
    474            insideConstructor = false;
    475        }
    476
    477        throw new RuntimeException(IOCMessages.instantiateBuilderError(moduleClass, fail), fail);
    478    }
    479
    480    private Object createProxy(ServiceResources resources, ObjectCreator creator, boolean preventDecoration)
    481    {
    482        String serviceId = resources.getServiceId();
    483        Class serviceInterface = resources.getServiceInterface();
    484
    485        String toString = format("<Proxy for %s(%s)>", serviceId, serviceInterface.getName());
    486
    487        ServiceProxyToken token = SerializationSupport.createToken(serviceId);
    488
    489        final Class serviceImplementation = preventDecoration || serviceInterface == TypeCoercer.class ? null : resources.getServiceImplementation();
    490        return createProxyInstance(creator, token, serviceInterface, serviceImplementation, toString);
    491    }
    492
    493    private Object createProxyInstance(final ObjectCreator creator, final ServiceProxyToken token,
    494                                       final Class serviceInterface, final Class serviceImplementation, final String description)
    495    {
    496        ClassInstantiator instantiator = proxyFactory.createProxy(serviceInterface, serviceImplementation, new PlasticClassTransformer()
    497        {
    498            @Override
    499            public void transform(final PlasticClass plasticClass)
    500            {
    501                plasticClass.introduceInterface(Serializable.class);
    502
    503                final PlasticField creatorField = plasticClass.introduceField(ObjectCreator.class, "creator").inject(
    504                        creator);
    505
    506                final PlasticField tokenField = plasticClass.introduceField(ServiceProxyToken.class, "token").inject(
    507                        token);
    508
    509                PlasticMethod delegateMethod = plasticClass.introducePrivateMethod(serviceInterface.getName(),
    510                        "delegate", null, null);
    511
    512                // If not concerned with efficiency, this might be done with method advice instead.
    513                delegateMethod.changeImplementation(new InstructionBuilderCallback()
    514                {
    515                    @Override
    516                    public void doBuild(InstructionBuilder builder)
    517                    {
    518                        builder.loadThis().getField(creatorField);
    519                        builder.invoke(ObjectCreator.class, Object.class, "createObject").checkcast(serviceInterface)
    520                                .returnResult();
    521                    }
    522                });
    523
    524                plasticClass.proxyInterface(serviceInterface, delegateMethod);
    525
    526                plasticClass.introduceMethod(WRITE_REPLACE).changeImplementation(new InstructionBuilderCallback()
    527                {
    528                    @Override
    529                    public void doBuild(InstructionBuilder builder)
    530                    {
    531                        builder.loadThis().getField(tokenField).returnResult();
    532                    }
    533                });
    534
    535                plasticClass.addToString(description);
    536            }
    537        }, false);
    538
    539        return instantiator.newInstance();
    540    }
    541
    542    @Override
    543    @SuppressWarnings("all")
    544    public Set<ContributionDef2> getContributorDefsForService(ServiceDef serviceDef)
    545    {
    546        Set<ContributionDef2> result = CollectionFactory.newSet();
    547
    548        for (ContributionDef next : moduleDef.getContributionDefs())
    549        {
    550            ContributionDef2 def = InternalUtils.toContributionDef2(next);
    551
    552            if (serviceDef.getServiceId().equalsIgnoreCase(def.getServiceId()))
    553            {
    554                result.add(def);
    555            } else
    556            {
    557                if (markerMatched(serviceDef, def))
    558                {
    559                    result.add(def);
    560                }
    561            }
    562        }
    563
    564        return result;
    565    }
    566
    567    private boolean markerMatched(ServiceDef serviceDef, Markable markable)
    568    {
    569        final Class markableInterface = markable.getServiceInterface();
    570
    571        if (markableInterface == null || !markableInterface.isAssignableFrom(serviceDef.getServiceInterface()))
    572            return false;
    573
    574        Set<Class> contributionMarkers = CollectionFactory.newSet(markable.getMarkers());
    575
    576        if (contributionMarkers.contains(Local.class))
    577        {
    578            // If @Local is present, filter out services that aren't in the same module.
    579            // Don't consider @Local to be a marker annotation
    580            // for the later match, however.
    581
    582            if (!isLocalServiceDef(serviceDef))
    583                return false;
    584
    585            contributionMarkers.remove(Local.class);
    586        }
    587
    588        // Filter out any stray annotations that aren't used by some
    589        // service, in any module, as a marker annotation.
    590
    591        contributionMarkers.retainAll(registry.getMarkerAnnotations());
    592
    593        //@Advise and @Decorate default to Object.class service interface.
    594        //If @Match is present, no marker annotations are needed.
    595        //In such a case an empty contribution marker list  should be ignored.
    596        if (markableInterface == Object.class && contributionMarkers.isEmpty())
    597            return false;
    598
    599        return serviceDef.getMarkers().containsAll(contributionMarkers);
    600    }
    601
    602    private boolean isLocalServiceDef(ServiceDef serviceDef)
    603    {
    604        return serviceDefs.containsKey(serviceDef.getServiceId());
    605    }
    606
    607    @Override
    608    public ServiceDef3 getServiceDef(String serviceId)
    609    {
    610        return serviceDefs.get(serviceId);
    611    }
    612
    613    @Override
    614    public String getLoggerName()
    615    {
    616        return moduleDef.getLoggerName();
    617    }
    618
    619    @Override
    620    public String toString()
    621    {
    622        return String.format("ModuleImpl[%s]", moduleDef.getLoggerName());
    623    }
    624
    625}