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