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