Java tutorial
/* * 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: case SVG: 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 final boolean compiling; 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; this.compiling = true; } public CompileContext(DefDescriptor<? extends Definition> topLevel) { this.clientLibs = null; this.topLevel = topLevel; this.shouldCacheDependencies = true; this.compiling = false; } 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 Collections.emptySet(); } @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 (compiling.def == null && !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 want to recurse in to properly include the defs when we // are compiling. Note that in this case, we already own the lock, so it can be outside the locking below. // When we are 'building' instead of 'compiling' we should already have the def somewhere, so we just // fill it in and continue. If no def is present, we explode. // if (currentCC != null) { if (currentCC.compiled.containsKey(descriptor)) { @SuppressWarnings("unchecked") CompilingDef<D> cd = (CompilingDef<D>) currentCC.compiled.get(descriptor); if (cd.def == null && !currentCC.compiling) { fillCompilingDef(cd, currentCC.context); } if (cd.def != null) { return cd.def; } } else if (!currentCC.compiling) { throw new IllegalStateException( "Attempting to add missing def " + descriptor + " to " + currentCC.topLevel); } // // 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); } public <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 (def == null) { return null; } // 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(); // } }