Package org.auraframework.impl.system

Source Code of org.auraframework.impl.system.MasterDefRegistryImpl$CompileContext

/*
* Copyright (C) 2013 salesforce.com, inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*         http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.auraframework.impl.system;

import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.locks.Lock;

import org.apache.log4j.Logger;
import org.auraframework.Aura;
import org.auraframework.adapter.ConfigAdapter;
import org.auraframework.cache.Cache;
import org.auraframework.def.AttributeDef;
import org.auraframework.def.BaseComponentDef;
import org.auraframework.def.ClientLibraryDef;
import org.auraframework.def.DefDescriptor;
import org.auraframework.def.DefDescriptor.DefType;
import org.auraframework.def.Definition;
import org.auraframework.def.DefinitionAccess;
import org.auraframework.def.DescriptorFilter;
import org.auraframework.def.RootDefinition;
import org.auraframework.service.CachingService;
import org.auraframework.service.LoggingService;
import org.auraframework.system.AuraContext;
import org.auraframework.system.DefRegistry;
import org.auraframework.system.DependencyEntry;
import org.auraframework.system.Location;
import org.auraframework.system.MasterDefRegistry;
import org.auraframework.system.Source;
import org.auraframework.throwable.AuraRuntimeException;
import org.auraframework.throwable.NoAccessException;
import org.auraframework.throwable.quickfix.DefinitionNotFoundException;
import org.auraframework.throwable.quickfix.QuickFixException;
import org.auraframework.util.text.GlobMatcher;
import org.auraframework.util.text.Hash;

import com.google.common.base.Optional;
import com.google.common.collect.ImmutableSortedSet;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;

import edu.umd.cs.findbugs.annotations.CheckForNull;
import edu.umd.cs.findbugs.annotations.NonNull;

/**
* Overall Master definition registry implementation, there be dragons here.
*
* This 'master' definition registry is actually a single threaded, per request registry that caches certain things in
* what is effectively a thread local cache. This means that once something is pulled into the local thread, it will not
* change.
*
*/
public class MasterDefRegistryImpl implements MasterDefRegistry {
    private static final Logger logger = Logger.getLogger(MasterDefRegistryImpl.class);

    private static final ImmutableSortedSet<String> cacheDependencyExceptions = ImmutableSortedSet.of(
            "apex://array",
            "apex://aura.component",
            "apex://blob",
            "apex://boolean",
            "apex://date",
            "apex://datetime",
            "apex://decimal",
            "apex://double",
            "apex://event",
            "apex://id",
            "apex://integer",
            "apex://list",
            "apex://long",
            "apex://map",
            "apex://object",
            "apex://set",
            "apex://string",
            "apex://sobject",
            "apex://time"
            );
    private final static int ACCESS_CHECK_CACHE_SIZE = 4096;

    private final Lock rLock;
    private final Lock wLock;

    private final Cache<DefDescriptor<?>, Boolean> existsCache;
    private final Cache<DefDescriptor<?>, Optional<? extends Definition>> defsCache;
    private final Cache<String, DependencyEntry> depsCache;
    private final Cache<String, String> stringsCache;
    private final Cache<String, Set<DefDescriptor<?>>> descriptorFilterCache;
    private final Cache<String, String> accessCheckCache;

    /**
     * A local dependencies cache.
     *
     * We store both by descriptor and by uid. The descriptor keys must include the type, as the qualified name is not
     * sufficient to distinguish it. In the case of the UID, we presume that we are safe.
     *
     * The two keys stored in the local cache are:
     * <ul>
     * <li>The UID, which should be sufficiently unique for a single request.</li>
     * <li>The type+qualified name of the descriptor. We store this to avoid construction in the case where we don't
     * have a UID. This is presumed safe because we assume that a single session will have a consistent set of
     * permissions</li>
     * </ul>
     */
    private final Map<String, DependencyEntry> localDependencies;

    private final RegistryTrie delegateRegistries;

    private final Map<DefDescriptor<? extends Definition>, Definition> defs;

    private Set<DefDescriptor<? extends Definition>> localDescs;

    private CompileContext currentCC;

    private final MasterDefRegistryImpl original;

    private MasterDefRegistryImpl(RegistryTrie delegate, MasterDefRegistryImpl original) {
        CachingService acs = Aura.getCachingService();

        this.delegateRegistries = delegate;
        this.original = original;
        this.rLock = acs.getReadLock();
        this.wLock = acs.getWriteLock();
        this.existsCache = acs.getExistsCache();
        this.defsCache = acs.getDefsCache();
        this.depsCache = acs.getDepsCache();
        this.stringsCache = acs.getStringsCache();
        this.descriptorFilterCache = acs.getDescriptorFilterCache();
        this.accessCheckCache = acs.<String, String> getCacheBuilder()
                .setInitialSize(ACCESS_CHECK_CACHE_SIZE)
                .setMaximumSize(ACCESS_CHECK_CACHE_SIZE)
                .setRecordStats(true)
                .setSoftValues(true)
                .build();
        this.localDependencies = Maps.newHashMap();
        this.defs = Maps.newHashMap();
        this.localDescs = null;
        this.currentCC = null;
    }

    /**
     * Build a system def registry that is meant to be used as a shadowing registry.
     *
     * This builds a registry that will not add new defs to the def set, and will allow any access (the access checks
     * MUST have been done before this is instantiated). Note that none of the defs built off of this will be sent to
     * the client, so it should be safe to allow this access.
     *
     * @param original the registry that is the 'public' registry.
     */
    public MasterDefRegistryImpl(@NonNull MasterDefRegistryImpl original) {
        this(original.delegateRegistries, original);
    }

    /**
     * Build a master def registry with a set of registries.
     *
     * This is the normal constructor for a master def registry.
     *
     * @param registries the registries to use in the mdr.
     */
    public MasterDefRegistryImpl(@NonNull DefRegistry<?>... registries) {
        this(new RegistryTrie(registries), null);
    }

    private boolean isOkForDependencyCaching(DefDescriptor<?> descriptor) {
        // if compound, OK as these tests are also conducted on the compound's target
        if (descriptor.getPrefix().equals("compound")) {
            return true;
        }

        // test cacheDependencyExceptions (like static types in Apex)
        String descriptorName = descriptor.getQualifiedName().toLowerCase();

        // truncate array markers
        if (descriptorName.endsWith("[]")) {
            descriptorName = descriptorName.substring(0, descriptorName.length() - 2);
        }
        if (cacheDependencyExceptions.contains(descriptorName)) {
            return true;
        }
        return false;
    }

    @Override
    @NonNull
    public Set<DefDescriptor<?>> find(@NonNull DescriptorFilter matcher) {
        final String filterKey = matcher.toString();
        Set<DefRegistry<?>> registries = delegateRegistries.getRegistries(matcher);
        Set<DefDescriptor<?>> matched = Sets.newHashSet();

        rLock.lock();
        try {
            boolean cacheable = shouldCache(matcher);
            for (DefRegistry<?> reg : registries) {
                //
                // This could be a little dangerous, but unless we force all of our
                // registries to implement find, this is necessary.
                //
                if (reg.hasFind()) {
                    Set<DefDescriptor<?>> registryResults = null;

                    if (cacheable && reg.isCacheable()) {
                        // cache results per registry
                        String cacheKey = filterKey + "|" + reg.toString();
                        registryResults = descriptorFilterCache.getIfPresent(cacheKey);
                        if (registryResults == null) {
                            registryResults = reg.find(matcher);
                            descriptorFilterCache.put(cacheKey, registryResults);
                        }
                    } else {
                        registryResults = reg.find(matcher);
                    }

                    matched.addAll(registryResults);
                }
            }
            if (localDescs != null) {
                for (DefDescriptor<? extends Definition> desc : localDescs) {
                    if (matcher.matchDescriptor(desc)) {
                        matched.add(desc);
                    }
                }
            }
        } finally {
            rLock.unlock();
        }

        return matched;
    }

    @Override
    @NonNull
    public <D extends Definition> Set<DefDescriptor<D>> find(@NonNull DefDescriptor<D> matcher) {
        Set<DefDescriptor<D>> matched;
        if (matcher.getNamespace().equals("*")) {
            matched = new LinkedHashSet<>();
            String qualifiedNamePattern = null;
            switch (matcher.getDefType()) {
            case CONTROLLER:
            case TESTSUITE:
            case MODEL:
            case RENDERER:
            case HELPER:
            case STYLE:
            case TYPE:
            case RESOURCE:
            case PROVIDER:
            case THEME_PROVIDER:
            case INCLUDE:
            case THEME_MAP_PROVIDER:
                qualifiedNamePattern = "%s://%s.%s";
                break;
            case ATTRIBUTE:
            case LAYOUT:
            case LAYOUT_ITEM:
            case TESTCASE:
            case APPLICATION:
            case COMPONENT:
            case INTERFACE:
            case EVENT:
            case LIBRARY:
            case DOCUMENTATION:
            case LAYOUTS:
            case NAMESPACE:
            case THEME:
            case THEME_DEF_REF:
            case VAR:
            case DESIGN:
            case ATTRIBUTE_DESIGN:
            case DESIGN_TEMPLATE:
            case DESIGN_TEMPLATE_REGION:
                qualifiedNamePattern = "%s://%s:%s";
                break;
            case ACTION:
            case DESCRIPTION:
            case EXAMPLE:
            case INCLUDE_REF:
                // TODO: FIXME
                throw new AuraRuntimeException(String.format("Find on %s defs not supported.", matcher.getDefType()
                        .name()));
            }
            rLock.lock();
            try {
                for (String namespace : delegateRegistries.getAllNamespaces()) {
                    String qualifiedName = String.format(qualifiedNamePattern,
                            matcher.getPrefix() != null ? matcher.getPrefix() : "*", namespace,
                            matcher.getName() != null ? matcher.getName() : "*");
                    @SuppressWarnings("unchecked")
                    DefDescriptor<D> namespacedMatcher = (DefDescriptor<D>) DefDescriptorImpl.getInstance(
                            qualifiedName,
                            matcher.getDefType().getPrimaryInterface());
                    DefRegistry<D> registry = getRegistryFor(namespacedMatcher);
                    if (registry != null) {
                        matched.addAll(registry.find(namespacedMatcher));
                    }
                }
            } finally {
                rLock.unlock();
            }
        } else {
            rLock.lock();
            try {
                matched = getRegistryFor(matcher).find(matcher);
            } finally {
                rLock.unlock();
            }
        }
        if (localDescs != null) {
            DescriptorFilter filter = new DescriptorFilter(matcher.getQualifiedName());
            for (DefDescriptor<? extends Definition> desc : localDescs) {
                if (filter.matchDescriptor(desc)) {
                    @SuppressWarnings("unchecked")
                    DefDescriptor<D> localDesc = (DefDescriptor<D>) desc;
                    matched.add(localDesc);
                }
            }
        }
        return matched;
    }

    /**
     * A compiling definition.
     *
     * This embodies a definition that is in the process of being compiled. It stores the descriptor, definition, and
     * the registry to which it belongs to avoid repeated lookups.
     */
    private static class CompilingDef<T extends Definition> implements Comparable<CompilingDef<?>> {
        public CompilingDef(@NonNull DefDescriptor<T> descriptor) {
            this.descriptor = descriptor;
        }

        /**
         * The descriptor we are compiling.
         */
        @NonNull
        public DefDescriptor<T> descriptor;

        /**
         * The compiled def.
         *
         * Should be non-null by the end of compile.
         */
        public T def;

        /**
         * Did we build this definition?.
         *
         * If this is true, we need to do the validation steps after finishing.
         */
        public boolean built = false;

        /**
         * The 'level' of this def in the compile tree.
         */
        public int level = 0;

        /**
         * Is this def cacheable?
         */
        public boolean cacheable = false;

        /**
         * have we validated this def yet?
         */
        public boolean validated = false;

        @Override
        public String toString() {
            StringBuffer sb = new StringBuffer();

            sb.append(descriptor);
            if (def != null) {
                sb.append("[");
                sb.append(def.getOwnHash());
                sb.append("]");

                sb.append("<");
                sb.append(level);
                sb.append(">");
            } else {
                sb.append("[not-compiled]");
            }
            sb.append(" : built=");
            sb.append(built);
            sb.append(", cacheable=");
            sb.append(cacheable);
            return sb.toString();
        }

        @Override
        public int compareTo(CompilingDef<?> o) {
            if (o.level != this.level) {
                return o.level - this.level;
            }
            return this.descriptor.compareTo(o.descriptor);
        }
    }

    /**
     * The compile context.
     *
     * This class holds the local information necessary for compilation.
     */
    private static class CompileContext {
        public final AuraContext context = Aura.getContextService().getCurrentContext();
        public final LoggingService loggingService = Aura.getLoggingService();
        public final Map<DefDescriptor<? extends Definition>, CompilingDef<?>> compiled = Maps.newHashMap();
        public final List<ClientLibraryDef> clientLibs;
        public final DefDescriptor<? extends Definition> topLevel;
        public int level;

        /** Is this def's dependencies cacheable? */
        public boolean shouldCacheDependencies;

        public CompileContext(DefDescriptor<? extends Definition> topLevel, List<ClientLibraryDef> clientLibs) {
            this.clientLibs = clientLibs;
            this.topLevel = topLevel;
            this.level = 0;
            this.shouldCacheDependencies = true;
        }

        public CompileContext(DefDescriptor<? extends Definition> topLevel) {
            this.clientLibs = null;
            this.topLevel = topLevel;
            this.shouldCacheDependencies = true;
        }

        public <D extends Definition> CompilingDef<D> getCompiling(DefDescriptor<D> descriptor) {
            @SuppressWarnings("unchecked")
            CompilingDef<D> cd = (CompilingDef<D>) compiled.get(descriptor);
            if (cd == null) {
                cd = new CompilingDef<>(descriptor);
                compiled.put(descriptor, cd);
            }
            return cd;
        }
    }

    /**
     * Check to see if we have a def locally.
     */
    private boolean hasLocalDef(DefDescriptor<?> descriptor) {
        return (original != null && original.defs.containsKey(descriptor)) || defs.containsKey(descriptor);
    }

    private <D extends Definition> D getLocalDef(DefDescriptor<D> descriptor) {
        if (original != null && original.defs.containsKey(descriptor)) {
            @SuppressWarnings("unchecked")
            D origDef = (D) original.defs.get(descriptor);
            return origDef;
        }
        if (defs.containsKey(descriptor)) {
            @SuppressWarnings("unchecked")
            D localDef = (D) defs.get(descriptor);
            return localDef;
        }
        return null;
    }

    /**
     * Fill a compiling def for a descriptor.
     *
     * This makes sure that we can get a registry for a given def, then tries to get the def from the global cache, if
     * that fails, it retrieves from the registry, and marks the def as locally built.
     *
     * @param compiling the current compiling def (if there is one).
     * @throws QuickFixException if validateDefinition caused a quickfix.
     */
    private <D extends Definition> boolean fillCompilingDef(CompilingDef<D> compiling, AuraContext context)
            throws QuickFixException {
        assert compiling.def == null;

        //
        // First, check our local cached defs to see if we have a fully compiled version.
        // in this case, we don't care about caching, since we are done.
        //
        if (hasLocalDef(compiling.descriptor)) {
            D localDef = getLocalDef(compiling.descriptor);
            if (localDef != null) {
                compiling.def = localDef;
                // I think this is no longer possible.
                compiling.built = !localDef.isValid();
                if (compiling.built) {
                    localDef.validateDefinition();
                }
                return true;
            } else {
                return false;
            }
        }

        //
        // If there is no local cache, we must first check to see if there is a registry, as we may not have
        // a registry (depending on configuration). In the case that we don't find one, we are done here.
        //
        DefRegistry<D> registry = getRegistryFor(compiling.descriptor);
        if (registry == null) {
            defs.put(compiling.descriptor, null);
            return false;
        }

        //
        // Now, check if we can cache the def later, as we won't have the registry to check at a later time.
        // If we can cache, look it up in the cache. If we find it, we have a built definition.
        // Currently, static registries are neither cached, nor do they affect dependency caching
        //
        if (!registry.isStatic()) {
            if (registry.isCacheable() && shouldCache(compiling.descriptor)) {
                compiling.cacheable = true;

                @SuppressWarnings("unchecked")
                Optional<D> opt = (Optional<D>) defsCache.getIfPresent(compiling.descriptor);
                if (opt != null) {
                    D cachedDef = opt.orNull();

                    if (cachedDef != null) {
                        @SuppressWarnings("unchecked")
                        DefDescriptor<D> canonical = (DefDescriptor<D>) cachedDef.getDescriptor();

                        compiling.def = cachedDef;
                        compiling.descriptor = canonical;
                        compiling.built = false;
                        return true;
                    } else {
                        return false;
                    }
                }
            } else {
                // if not a cacheable registry or not shouldCache, test other exceptions that might still
                // allow dependency caching (if it's from static registry, it can't affect our decision on
                // depsCaching) test for special cases: compounds and static apex types
                boolean qualified = isOkForDependencyCaching(compiling.descriptor);

                currentCC.shouldCacheDependencies = qualified;
            }
        }

        //
        // The last case. This is our first compile or the def is uncacheable.
        // In this case, we make sure that the initial validation is called, and put
        // the def in the 'built' set.
        //
        compiling.def = registry.getDef(compiling.descriptor);
        if (compiling.def == null) {
            return false;
        }
        @SuppressWarnings("unchecked")
        DefDescriptor<D> canonical = (DefDescriptor<D>) compiling.def.getDescriptor();
        compiling.descriptor = canonical;
        currentCC.loggingService.incrementNum(LoggingService.DEF_COUNT);
        context.pushCallingDescriptor(canonical);
        try {
            compiling.def.validateDefinition();
        } finally {
            context.popCallingDescriptor();
        }
        compiling.built = true;
        return true;
    }

    /**
     * A private helper routine to make the compiler code more sane.
     *
     * @param descriptor the descriptor that we are currently handling, must not be in the compiling defs.
     * @param cc the compile context to allow us to accumulate information.
     * @param stack the incoming stack (linked hash set, so order is preserved).
     * @param parent the direct parent of the definition we are looking up.
     * @throws QuickFixException if the definition is not found, or validateDefinition() throws one.
     */
    private <D extends Definition> D getHelper(@NonNull DefDescriptor<D> descriptor,
            @NonNull CompileContext cc, @NonNull Set<DefDescriptor<?>> stack,
            @CheckForNull Definition parent) throws QuickFixException {
        currentCC.loggingService.incrementNum(LoggingService.DEF_VISIT_COUNT);
        if (stack.contains(descriptor)) {
            // System.out.println("cycle at "+stack+" "+descriptor);
            return null;
        }
        CompilingDef<D> cd = cc.getCompiling(descriptor);
        if (cc.level > cd.level) {
            cd.level = cc.level;
            if (cd.def != null) {
                // System.out.println("recalculating at "+stack+" "+descriptor);
            }
        } else {
            if (cd.def != null) {
                return cd.def;
            }
        }
        cc.level += 1;
        stack.add(descriptor);
        try {
            //
            // careful here. We don't just return with the non-null def because that breaks our levels.
            // We need to walk the whole tree, which is unfortunate perf-wise.
            //
            if (cd.def == null) {
                if (!fillCompilingDef(cd, cc.context)) {
                    // No def. Blow up.
                    Location l = null;
                    if (parent != null) {
                        l = parent.getLocation();
                    }
                    stack.remove(descriptor);
                    throw new DefinitionNotFoundException(descriptor, l, stack.toString());
                }
                // get client libs
                if (cc.clientLibs != null && cd.def instanceof BaseComponentDef) {
                    BaseComponentDef baseComponent = (BaseComponentDef) cd.def;
                    baseComponent.addClientLibs(cc.clientLibs);
                }
            }

            Set<DefDescriptor<?>> newDeps = Sets.newHashSet();
            cd.def.appendDependencies(newDeps);

            for (DefDescriptor<?> dep : newDeps) {
                getHelper(dep, cc, stack, cd.def);
            }

            return cd.def;
        } finally {
            cc.level -= 1;
            stack.remove(descriptor);
        }
    }

    /**
     * finish up the validation of a set of compiling defs.
     *
     * @param context only needed to do setCurrentNamspace.
     */
    private void finishValidation() throws QuickFixException {
        int iteration = 0;
        List<CompilingDef<?>> compiling = null;

        //
        // Validate our references. This part is uh, painful.
        // Turns out that validating references can pull in things we didn't see, so we
        // loop infinitely... or at least a few times.
        //
        // This can be changed once we remove the ability to nest, as we will never allow
        // this. That way we won't have to copy our list so many times.
        //
        do {
            compiling = Lists.newArrayList(currentCC.compiled.values());

            for (CompilingDef<?> cd : compiling) {
                currentCC.context.pushCallingDescriptor(cd.descriptor);
                try {
                    if (cd.built && !cd.validated) {
                        if (iteration != 0) {
                            logger.warn("Nested add of " + cd.descriptor + " during validation of "
                                    + currentCC.topLevel);
                            // throw new
                            // AuraRuntimeException("Nested add of "+cd.descriptor+" during validation of "+currentCC.topLevel);
                        }
                        cd.def.validateReferences();
                        cd.validated = true;
                    }
                } finally {
                    currentCC.context.popCallingDescriptor();
                }
            }
            iteration += 1;
        } while (compiling.size() < currentCC.compiled.size());

        //
        // And finally, mark everything as happily compiled.
        //
        for (CompilingDef<?> cd : compiling) {
            if (cd.def == null) {
                throw new AuraRuntimeException("Missing def for " + cd.descriptor + " during validation of "
                        + currentCC.topLevel);
            }
            if (cd.def != null) {
                defs.put(cd.descriptor, cd.def);
                if (cd.built) {
                    if (cd.cacheable) { // false for non-privileged namespaces, or non-cacheable registries
                        defsCache.put(cd.descriptor, Optional.of(cd.def));
                    }
                    cd.def.markValid();
                }
            }
        }
    }

    /**
     * Compile a single definition, finding all of the static dependencies.
     *
     * This is the primary entry point for compiling a single definition. The basic guarantees enforced here are:
     * <ol>
     * <li>Each definition has 'validateDefinition()' called on it exactly once.</li>
     * <li>No definition is marked as valid until all definitions in the dependency set have been validated</li>
     * <li>Each definition has 'validateReferences()' called on it exactly once, after the definitions have been put in
     * local cache</li>
     * <li>All definitions are marked valid by the DefRegistry after the validation is complete</li>
     * <li>No definition should be available to other threads until it is marked valid</li>
     * <ol>
     *
     * In order to do all of this, we keep a set of 'compiling' definitions locally, and use that to calculate
     * dependencies and walk the tree. Circular dependencies are handled gracefully, and no other thread can interfere
     * because everything is local.
     *
     * FIXME: this should really cache invalid definitions and make sure that we don't bother re-compiling until there
     * is some change of state. However, that is rather more complex than it sounds.... and shouldn't really manifest
     * much in a released system.
     *
     * @param descriptor the descriptor that we wish to compile.
     */
    @CheckForNull
    private <D extends Definition> D compileDef(@NonNull DefDescriptor<D> descriptor, @NonNull CompileContext cc)
            throws QuickFixException {
        D def;
        boolean nested = (cc == currentCC);

        if (!nested && currentCC != null) {
            throw new AuraRuntimeException("Unexpected nesting of contexts. This is not allowed");
        }
        currentCC = cc;
        if (!nested) {
            currentCC.loggingService.startTimer(LoggingService.TIMER_DEFINITION_CREATION);
        }
        try {
            Set<DefDescriptor<?>> stack = Sets.newLinkedHashSet();
            try {
                def = getHelper(descriptor, currentCC, stack, null);
            } catch (DefinitionNotFoundException ndfe) {
                if (nested) {
                    // ooh, nasty, we might be in a 'failure is ok state', in which case
                    // we need to be sure that we don't mess up the finishValidation step
                    // by leaving an empty entry around... If failure is _not_ ok, the next
                    // level up will break.
                    if (currentCC.compiled.containsKey(descriptor)) {
                        currentCC.compiled.remove(descriptor);
                    }
                }
                if (descriptor.equals(ndfe.getDescriptor())) {
                    //
                    // ignore a nonexistent def here.
                    //
                    return null;
                } else {
                    throw ndfe;
                }
            }

            if (!nested) {
                finishValidation();
            }
            return def;
        } finally {
            if (!nested) {
                currentCC.loggingService.stopTimer(LoggingService.TIMER_DEFINITION_CREATION);
                currentCC = null;
            }
        }
    }

    /**
     * Internal routine to compile and return a DependencyEntry.
     *
     * This routine always compiles the definition, even if it is in the caches. If the incoming descriptor does not
     * correspond to a definition, it will return null, otherwise, on failure it will throw a QuickFixException.
     *
     * Please look at {@link #localDependencies} if you are mucking in here.
     *
     * Side Effects:
     * <ul>
     * <li>All definitions that were encountered during the compile will be put in the local def cache, even if a QFE is
     * thrown</li>
     * <li>A hash is compiled for the definition if it compiles</li>
     * <li>a dependency entry is cached locally in any case</li>
     * <li>a dependency entry is cached globally if the definition compiled</li>
     * </ul>
     *
     * @param descriptor the incoming descriptor to compile
     * @return the definition compiled from the descriptor, or null if not found.currentCC
     * @throws QuickFixException if the definition failed to compile.
     */
    @CheckForNull
    protected <T extends Definition> DependencyEntry compileDE(@NonNull DefDescriptor<T> descriptor)
            throws QuickFixException {
        // See localDependencies commentcurrentCC
        String key = makeLocalKey(descriptor);

        if (currentCC != null) {
            throw new AuraRuntimeException("Ugh, nested compileDE/buildDE on " + currentCC.topLevel
                    + " trying to build " + descriptor);
        }

        try {
            List<ClientLibraryDef> clientLibs = Lists.newArrayList();
            CompileContext cc = new CompileContext(descriptor, clientLibs);
            Definition def = compileDef(descriptor, cc);
            DependencyEntry de;
            String uid;

            if (def == null) {
                return null;
            }

            List<CompilingDef<?>> compiled = Lists.newArrayList(cc.compiled.values());
            Collections.sort(compiled);

            Set<DefDescriptor<? extends Definition>> deps = Sets.newLinkedHashSet();

            //
            // Now walk the sorted list, building up our dependencies, and uid
            //
            StringBuilder sb = new StringBuilder(256);
            Hash.StringBuilder globalBuilder = new Hash.StringBuilder();
            for (CompilingDef<?> cd : compiled) {
                if (cd.def == null) {
                    // actually, this should never happen.
                    throw new DefinitionNotFoundException(cd.descriptor);
                }

                deps.add(cd.descriptor);

                //
                // Now update our hash.
                //
                sb.setLength(0);
                sb.append(cd.descriptor.getQualifiedName().toLowerCase());
                sb.append("|");
                String hash = cd.def.getOwnHash();
                if (hash != null) {
                    sb.append(hash);
                }
                sb.append(",");
                globalBuilder.addString(sb.toString());
            }
            uid = globalBuilder.build().toString();

            //
            // Now try a re-lookup. This may catch existing cached
            // entries where uid was null.
            //
            de = getDE(uid, descriptor);
            if (de != null) {
                return de;
            }

            de = new DependencyEntry(uid, Collections.unmodifiableSet(deps), clientLibs);
            if (shouldCache(descriptor)) {
                // put UID-qualified descriptor key for dependency
                depsCache.put(makeGlobalKey(de.uid, descriptor), de);

                // put unqualified descriptor key for dependency
                if (cc.shouldCacheDependencies) {
                    depsCache.put(makeNonUidGlobalKey(descriptor), de);
                }
            }
            // See localDependencies comment
            localDependencies.put(de.uid, de);
            localDependencies.put(key, de);
            return de;
        } catch (QuickFixException qfe) {
            // See localDependencies comment
            localDependencies.put(key, new DependencyEntry(qfe));
            throw qfe;
        }
    }

    /**
     * Get a dependency entry for a given uid.
     *
     * This is a convenience routine to check both the local and global cache for a value.
     *
     * Please look at {@link #localDependencies} if you are mucking in here.
     *
     * Side Effects:
     * <ul>
     * <li>If a dependency is found in the global cache, it is populated into the local cache.</li>
     * </ul>
     *
     * @param uid the uid may be null, if so, it only checks the local cache.
     * @param descriptor the descriptor, used for both global and local cache lookups.
     * @return the DependencyEntry or null if none present.
     */
    private DependencyEntry getDE(@CheckForNull String uid, @NonNull DefDescriptor<?> descriptor) {
        // See localDependencies comment
        String key = makeLocalKey(descriptor);
        DependencyEntry de;

        if (uid != null) {
            de = localDependencies.get(uid);
            if (de != null) {
                return de;
            }
            if (shouldCache(descriptor)) {
                de = depsCache.getIfPresent(makeGlobalKey(uid, descriptor));
            }
        } else {
            // See localDependencies comment
            de = localDependencies.get(key);
            if (de != null) {
                return de;
            }
            if (shouldCache(descriptor)) {
                de = depsCache.getIfPresent(makeNonUidGlobalKey(descriptor));
            }
        }
        if (de != null) {
            // See localDependencies comment
            localDependencies.put(de.uid, de);
            localDependencies.put(key, de);
        }
        return de;
    }

    @Override
    public Set<DefDescriptor<?>> getDependencies(String uid) {
        DependencyEntry de = localDependencies.get(uid);

        if (de != null) {
            return de.dependencies;
        }
        return null;
    }

    @Override
    public List<ClientLibraryDef> getClientLibraries(String uid) {
        DependencyEntry de = localDependencies.get(uid);

        if (de != null) {
            return de.clientLibraries;
        }
        return null;
    }

    /**
     * Typesafe helper for getDef.
     *
     * This adds new definitions (unvalidated) to the list passed in. Definitions that were previously built are simply
     * added to the local cache.
     *
     * The quick fix exception case is actually a race condition where we previously had a set of depenendencies, and
     * something changed, making our set inconsistent. There are no guarantees that during a change all MDRs will have a
     * correct set of definitions.
     *
     * @param descriptor the descriptor for which we need a definition.
     * @return A compilingDef for the definition, or null if not needed.
     * @throws QuickFixException if something has gone terribly wrong.
     */
    private <D extends Definition> void validateHelper(@NonNull DefDescriptor<D> descriptor) throws QuickFixException {
        CompilingDef<D> compiling = new CompilingDef<>(descriptor);
        currentCC.compiled.put(descriptor, compiling);
    }

    /**
     * Build a DE 'in place' with no tree traversal.
     */
    private <D extends Definition> void buildDE(@NonNull DependencyEntry de, @NonNull DefDescriptor<?> descriptor)
            throws QuickFixException {
        if (currentCC != null) {
            throw new AuraRuntimeException("Ugh, nested compileDE/buildDE on " + currentCC.topLevel
                    + " trying to build " + descriptor);
        }
        currentCC = new CompileContext(descriptor);
        try {
            validateHelper(descriptor);
            for (DefDescriptor<?> dd : de.dependencies) {
                validateHelper(dd);
            }
            for (CompilingDef<?> compiling : currentCC.compiled.values()) {
                if (!fillCompilingDef(compiling, currentCC.context)) {
                    throw new DefinitionNotFoundException(descriptor);
                }
            }
            finishValidation();
        } finally {
            currentCC = null;
        }
    }

    /**
     * Get a definition.
     *
     * This does a scan of the loaded dependency entries to check if there is something to pull, otherwise, it just
     * compiles the entry. This should log a warning somewhere, as it is a dependency that was not noted.
     *
     * @param descriptor the descriptor to find.
     * @return the corresponding definition, or null if it doesn't exist.
     * @throws QuickFixException if there is a compile time error.
     */
    @Override
    @CheckForNull
    public <D extends Definition> D getDef(@CheckForNull DefDescriptor<D> descriptor) throws QuickFixException {
        if (descriptor == null) {
            return null;
        }

        //
        // Always check for a local def before locking.
        //
        if (hasLocalDef(descriptor)) {
            return getLocalDef(descriptor);
        }
        //
        // If our current context is not null, we always want to recurse
        // in to properly include the defs. Note that in this case, we already
        // own the lock, so it can be outside the locking below.
        //
        if (currentCC != null) {
            if (currentCC.compiled.containsKey(descriptor)) {
                @SuppressWarnings("unchecked")
                CompilingDef<D> cd = (CompilingDef<D>) currentCC.compiled.get(descriptor);
                if (cd.def != null) {
                    return cd.def;
                }
            }

            //
            // If we are nested, compileDef will do the right thing.
            // This is a bit ugly though.
            //
            return compileDef(descriptor, currentCC);
        }
        rLock.lock();
        try {
            DependencyEntry de = getDE(null, descriptor);
            if (de == null) {
                for (DependencyEntry det : localDependencies.values()) {
                    if (det.dependencies != null && det.dependencies.contains(descriptor)) {
                        de = det;
                        break;
                    }
                }

                if (de == null) {
                    compileDE(descriptor);

                    @SuppressWarnings("unchecked")
                    D def = (D) defs.get(descriptor);
                    return def;
                }
            }

            //
            // found an entry.
            // In this case, throw a QFE if we have one.
            //
            if (de.qfe != null) {
                throw de.qfe;
            }

            //
            // Now we need to actually do the build..
            //
            buildDE(de, descriptor);

            @SuppressWarnings("unchecked")
            D def = (D) defs.get(descriptor);
            return def;
        } finally {
            rLock.unlock();
        }
    }

    @SuppressWarnings("unchecked")
    @Override
    public <D extends Definition> void save(@NonNull D def) {
        wLock.lock();
        try {
            getRegistryFor((DefDescriptor<D>) def.getDescriptor()).save(def);
            invalidate(def.getDescriptor());
        } finally {
            wLock.unlock();
        }
    }

    @Override
    public <D extends Definition> boolean exists(DefDescriptor<D> descriptor) {
        boolean cacheable;
        boolean regExists;

        if (hasLocalDef(descriptor)) {
            return getLocalDef(descriptor) != null;
        }
        DefRegistry<D> reg = getRegistryFor(descriptor);
        if (reg == null) {
            return false;
        }
        cacheable = reg.isCacheable() && shouldCache(descriptor);
        if (cacheable) {
            //
            // Try our various caches.
            //
            Boolean val = existsCache.getIfPresent(descriptor);
            if (val != null && val.booleanValue()) {
                return true;
            }
        }
        rLock.lock();
        try {
            if (cacheable) {
                Optional<?> opt = defsCache.getIfPresent(descriptor);
                if (opt != null) {
                    //
                    // We cache here.
                    //
                    if (opt.isPresent()) {
                        existsCache.put(descriptor, Boolean.TRUE);
                        return true;
                    } else {
                        existsCache.put(descriptor, Boolean.FALSE);
                        return false;
                    }
                }
            }
            regExists = reg.exists(descriptor);
            if (cacheable) {
                Boolean cacheVal = Boolean.valueOf(regExists);
                existsCache.put(descriptor, cacheVal);
            }
        } finally {
            rLock.unlock();
        }
        if (regExists == false) {
            // Cache negatives to avoid excessive lookups.
            defs.put(descriptor, null);
        }
        return regExists;
    }

    /**
     * This figures out based on prefix what registry this component is for, it could return null if the prefix is not
     * found.
     *
     * Note: The generic typing here is incorrect, as in not true.
     */
    private <T extends Definition> DefRegistry<T> getRegistryFor(@NonNull DefDescriptor<T> descriptor) {
        @SuppressWarnings("unchecked")
        DefRegistry<T> reg = (DefRegistry<T>) delegateRegistries.getRegistryFor(descriptor);
        return reg;
    }

    @Override
    public <D extends Definition> void addLocalDef(D def) {
        DefDescriptor<? extends Definition> desc = def.getDescriptor();

        defs.put(desc, def);
        if (localDescs == null) {
            localDescs = Sets.newHashSet();
        }
        localDescs.add(desc);
    }

    @Override
    public <T extends Definition> Source<T> getSource(DefDescriptor<T> descriptor) {
        DefRegistry<T> reg = getRegistryFor(descriptor);
        if (reg != null) {
            return reg.getSource(descriptor);
        }
        return null;
    }

    @Override
    public boolean namespaceExists(String ns) {
        return delegateRegistries.getAllNamespaces().contains(ns);
    }

    @Override
    public <D extends Definition> void assertAccess(DefDescriptor<?> referencingDescriptor, D def)
            throws QuickFixException {
        assertAccess(referencingDescriptor, def, accessCheckCache);
    }

    <D extends Definition> void assertAccess(DefDescriptor<?> referencingDescriptor, D def,
            Cache<String, String> accessCheckCache) {
        String status = hasAccess(referencingDescriptor, def, accessCheckCache);
        if (status != null) {
            DefDescriptor<? extends Definition> descriptor = def.getDescriptor();
            String message = Aura.getConfigAdapter().isProduction() ? DefinitionNotFoundException.getMessage(
                    descriptor.getDefType(), descriptor.getName()) : status;
            throw new NoAccessException(message);
        }
    }

    @Override
    public <D extends Definition> String hasAccess(DefDescriptor<?> referencingDescriptor, D def) {
        return hasAccess(referencingDescriptor, def, accessCheckCache);
    }

    <D extends Definition> String hasAccess(DefDescriptor<?> referencingDescriptor, D def,
            Cache<String, String> accessCheckCache) {
        // If the def is access="global" or does not require authentication then anyone can see it
        DefinitionAccess access = def.getAccess();
        if (access.isGlobal() || !access.requiresAuthentication()) {
            return null;
        }

        ConfigAdapter configAdapter = Aura.getConfigAdapter();
        String referencingNamespace = null;
        if (referencingDescriptor != null) {
            String prefix = referencingDescriptor.getPrefix();
            if (configAdapter.isUnsecuredPrefix(prefix)) {
                return null;
            }

            referencingNamespace = referencingDescriptor.getNamespace();

            // The caller is in a system namespace let them through
            if (configAdapter.isPrivilegedNamespace(referencingNamespace)) {
                return null;
            }
        }

        DefDescriptor<?> desc = def.getDescriptor();

        String namespace;
        String target;
        if (def instanceof AttributeDef) {
            AttributeDef attributeDef = (AttributeDef) def;
            DefDescriptor<? extends RootDefinition> parentDescriptor = attributeDef.getParentDescriptor();
            namespace = parentDescriptor.getNamespace();
            target = String.format("%s:%s.%s", namespace, parentDescriptor.getName(), desc.getName());
        } else {
            namespace = desc.getNamespace();
            target = String.format("%s:%s", namespace, desc.getName());
        }

        // Cache key is of the form "referencingNamespace>defNamespace:defName[.subDefName].defTypeOrdinal"
        DefType defType = desc.getDefType();
        String key = String.format("%s>%s.%d", referencingNamespace == null ? "" : referencingNamespace, target,
                defType.ordinal());
        String status = accessCheckCache.getIfPresent(key);
        if (status == null) {
            status = "";

            // Protect against re-entry
            accessCheckCache.put(key, status);

            // System.out.printf("** MDR.miss.assertAccess() cache miss for: %s\n", key);

            DefDescriptor<? extends Definition> descriptor = def.getDescriptor();
            if (!configAdapter.isUnsecuredNamespace(namespace)
                    && !configAdapter.isUnsecuredPrefix(descriptor.getPrefix())) {
                if (referencingNamespace == null || referencingNamespace.isEmpty()) {
                    status = String
                            .format("Access to %s '%s' disallowed by MasterDefRegistry.assertAccess(): referencing namespace was empty or null",
                                    defType, target);
                } else if (!referencingNamespace.equals(namespace)) {
                    // The caller and the def are not in the same namespace
                    status = String
                            .format("Access to %s '%s' from namespace '%s' in '%s(%s)' disallowed by MasterDefRegistry.assertAccess()",
                                    defType.toString().toLowerCase(), target, referencingNamespace,
                                    referencingDescriptor, referencingDescriptor.getDefType());
                }
            }

            if (!status.isEmpty()) {
                accessCheckCache.put(key, status);
            }
        } else {
            // System.out.printf("** MDR.hit.assertAccess() cache hit for: %s\n", key);
        }

        return status.isEmpty() ? null : status;
    }

    /**
     * only used by admin tools to view all registries
     */
    public DefRegistry<?>[] getAllRegistries() {
        return delegateRegistries.getAllRegistries();
    }

    /**
     * Filter the entire set of current definitions by a set of preloads.
     *
     * This filtering is very simple, it just looks for local definitions that are not included in the preload set.
     */
    @Override
    public Map<DefDescriptor<? extends Definition>, Definition> filterRegistry(Set<DefDescriptor<?>> preloads) {
        Map<DefDescriptor<? extends Definition>, Definition> filtered;

        if (preloads == null || preloads.isEmpty()) {
            return Maps.newHashMap(defs);
        }
        filtered = Maps.newHashMapWithExpectedSize(defs.size());
        for (Map.Entry<DefDescriptor<? extends Definition>, Definition> entry : defs.entrySet()) {
            if (!preloads.contains(entry.getKey())) {
                filtered.put(entry.getKey(), entry.getValue());
            }
        }
        return filtered;
    }

    @Override
    public <T extends Definition> boolean invalidate(DefDescriptor<T> descriptor) {
        defs.clear();
        if (localDescs != null) {
            localDescs.clear();
        }
        localDependencies.clear();
        if (shouldCache(descriptor)) {
            depsCache.invalidateAll();
            defsCache.invalidateAll();
            existsCache.invalidateAll();
            descriptorFilterCache.invalidateAll();
        }
        return false;
    }

    private String getKey(DependencyEntry de, DefDescriptor<?> descriptor, String key) {
        return String.format("%s@%s@%s", de.uid, descriptor.getQualifiedName().toLowerCase(), key);
    }

    @Override
    public String getCachedString(String uid, DefDescriptor<?> descriptor, String key) {
        if (shouldCache(descriptor)) {
            DependencyEntry de = localDependencies.get(uid);

            if (de != null) {
                return stringsCache.getIfPresent(getKey(de, descriptor, key));
            }
        }
        return null;
    }

    @Override
    public void putCachedString(String uid, DefDescriptor<?> descriptor, String key, String value) {
        if (shouldCache(descriptor)) {
            DependencyEntry de = localDependencies.get(uid);

            if (de != null) {
                stringsCache.put(getKey(de, descriptor, key), value);
            }
        }
    }

    /**
     * Get the UID.
     *
     * This uses some trickery to try to be efficient, including using a dual keyed local cache to avoid looking up
     * values more than once even in the absense of remembered context.
     *
     * Note: there is no guarantee that the definitions have been fetched from cache here, so there is a very subtle
     * race condition.
     *
     * Also note that this _MUST NOT_ be called inside of a compile, or things may get out of wack. We probably should
     * be asserting this somewhere.
     *
     * @param uid the uid for cache lookup (null means unknown).
     * @param descriptor the descriptor to fetch.
     * @return the correct uid for the definition, or null if there is none.
     * @throws QuickFixException if the definition cannot be compiled.
     */
    @Override
    public <T extends Definition> String getUid(String uid, DefDescriptor<T> descriptor) throws QuickFixException {
        if (descriptor == null) {
            return null;
        }

        DependencyEntry de = null;

        rLock.lock();
        try {
            de = getDE(uid, descriptor);
            if (de == null) {
                try {
                    de = compileDE(descriptor);
                    //
                    // If we can't find our descriptor, we just give back a null.
                    if (de == null) {
                        return null;
                    }
                } catch (QuickFixException qfe) {
                    // try to pick it up from the cache.
                    de = getDE(null, descriptor);
                    // this should never happen.
                    if (de == null) {
                        throw new AuraRuntimeException("unexpected null on QFE");
                    }
                }
            }
        } finally {
            rLock.unlock();
        }
        if (de.qfe != null) {
            throw de.qfe;
        }
        return de.uid;
    }

    /** Creates a key for the localDependencies, using DefType and FQN. */
    private String makeLocalKey(@NonNull DefDescriptor<?> descriptor) {
        return descriptor.getDefType().toString() + ":" + descriptor.getQualifiedName().toLowerCase();
    }

    /**
     * Creates a key for the global {@link #depsCache}, using UID, type, and FQN.
     */
    private String makeGlobalKey(String uid, @NonNull DefDescriptor<?> descriptor) {
        return uid + "/" + makeLocalKey(descriptor);
    }

    /**
     * Creates a key for the global {@link #depsCache}, using only descriptor (and Mode internally).
     *
     * @param descriptor - the descriptor use for the key
     */
    private String makeNonUidGlobalKey(@NonNull DefDescriptor<?> descriptor) {
        return makeLocalKey(descriptor);
    }

    /**
     * Return true if the namespace of the provided descriptor supports caching.
     */
    private boolean shouldCache(DefDescriptor<?> descriptor) {
        if (descriptor == null) {
            return false;
        }
        String prefix = descriptor.getPrefix();
        String namespace = descriptor.getNamespace();
        return shouldCache(prefix, namespace);
    }

    /**
     * Return true if the descriptor filter meets all requirements for the result of find to be cached
     */
    private boolean shouldCache(DescriptorFilter filter) {
        GlobMatcher p = filter.getPrefixMatch();
        String prefix = ((p.isConstant()) ? p.toString() : null);

        GlobMatcher ns = filter.getNamespaceMatch();
        String namespace = ((ns.isConstant()) ? ns.toString() : null);

        return (prefix != null || namespace != null) && shouldCache(prefix, namespace);
    }

    /**
     * Return true if the namespace supports cacheing
     */
    private boolean shouldCache(String prefix, String namespace) {
        boolean cacheable = false;
        if (namespace == null) {
            if (prefix == null) {
                cacheable = false;
            } else {
                ConfigAdapter configAdapter = Aura.getConfigAdapter();
                cacheable = configAdapter.isCacheablePrefix(prefix);
            }
        } else if (prefix == null) {
            ConfigAdapter configAdapter = Aura.getConfigAdapter();
            cacheable = configAdapter.isPrivilegedNamespace(namespace);
        } else {
            ConfigAdapter configAdapter = Aura.getConfigAdapter();
            cacheable = configAdapter.isCacheablePrefix(prefix) || configAdapter.isPrivilegedNamespace(namespace);
        }
        return cacheable;
    }

    // TODO - W-2105858 - re-enable with either the private implementation of the Cache used, or
    // a least-common-denominator implementation

    // public static Collection<Optional<? extends Definition>> getCachedDefs() {
    // return defsCache.asMap().values();
    // }
    //
    // public static CacheStats getDefsCacheStats() {
    // return defsCache.stats();
    // }
    //
    // public static CacheStats getExistsCacheStats() {
    // return existsCache.stats();
    // }
    //
    // public static CacheStats getStringsCacheStats() {
    // return stringsCache.stats();
    // }
    //
    // public static CacheStats getDescriptorFilterCacheStats() {
    // return descriptorFilterCache.stats();
    // }
}
TOP

Related Classes of org.auraframework.impl.system.MasterDefRegistryImpl$CompileContext

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.