/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.netbeans.core.startup;

import java.io.DataOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.jar.Attributes;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.netbeans.Events;
import org.netbeans.InvalidException;
import org.netbeans.Module;
import org.netbeans.ModuleInstaller;
import org.netbeans.ModuleManager;
import org.netbeans.Stamps;
import org.netbeans.Util;
import org.netbeans.core.startup.layers.ModuleLayeredFileSystem;
import org.openide.filesystems.FileUtil;
import org.openide.modules.Dependency;
import org.openide.modules.ModuleInfo;
import org.openide.modules.ModuleInstall;
import org.openide.modules.SpecificationVersion;
import org.openide.util.BaseUtilities;
import org.openide.util.NbCollections;
import org.openide.util.SharedClassObject;
import org.openide.util.NbBundle;
import org.openide.util.Task;
import org.openide.util.lookup.InstanceContent;


/** Concrete implementation of the module installation functionality.
 * This class can pay attention to the details of manifest format,
 * details of how to install particular sections and layers and so on.
 * It may have a limited UI, i.e. notifying problems with localized
 * messages and the like.
 * @author Jesse Glick, Jan Pokorsky, Jaroslav Tulach, et al.
 */
final class NbInstaller extends ModuleInstaller {

    private static final Logger LOG = Logger.getLogger(NbInstaller.class.getName());

    /** set of manifest sections for each module */
    private final Map<Module,Set<ManifestSection>> sections = new HashMap<Module,Set<ManifestSection>>(100);
    /** ModuleInstall classes for each module that declares one */
    private final Map<Module,Class<? extends ModuleInstall>> installs = new HashMap<Module,Class<? extends ModuleInstall>>(100);
    /** layer resources for each module that declares one */
    private final Map<Module,String> layers = new HashMap<Module,String>(100);
    /** exact use of this is hard to explain */
    private boolean initializedFolderLookup = false;
    /** where to report events to */
    private final Events ev;
    /** associated controller of module list; needed for handling ModuleInstall ser */
    private ModuleList moduleList;
    /** associated manager */
    private ModuleManager mgr;
    /** set of permitted core or package dependencies from a module */
    private final Map<Module,Set<String>> kosherPackages = new HashMap<Module,Set<String>>(100);
    /** classpath ~ JRE packages to be hidden from a module */
    private final Map<Module,List<Module.PackageExport>> hiddenClasspathPackages = new  HashMap<Module,List<Module.PackageExport>>();
    /** #164510: similar to {@link #hiddenClasspathPackages} but backwards for efficiency */
    private final Map<Module.PackageExport,List<Module>> hiddenClasspathPackagesReverse = new HashMap<Module.PackageExport,List<Module>>();
    /** caches important values from module manifests */
    private final Cache cache = new Cache();
    /** Processing @OnStart/@OnStop calls */
    private final NbStartStop onStartStop = new NbStartStop(null, null);
        
    /** Create an NbInstaller.
     * You should also call {@link #registerManager} and if applicable
     * {@link #registerList} to make the installer fully usable.
     * @param ev events to log
     */
    public NbInstaller(Events ev) {
        this.ev = ev;
    }
    
    /** Access from ModuleSystem. */
    void registerList(ModuleList list) {
        if (moduleList != null) throw new IllegalStateException();
        moduleList = list;
    }
    void registerManager(ModuleManager manager) {
        if (mgr != null) throw new IllegalStateException();
        mgr = manager;
    }

    // @SuppressWarnings("unchecked")
    @Override
    public void prepare(Module m) throws InvalidException {
        ev.log(Events.PREPARE, m);
        checkForHiddenPackages(m);
        Set<ManifestSection> mysections = null;
        Class<?> clazz = null;
        
        String processSections = cache.findGlobalProperty("processSections", null, "false"); // NOI18N
        if (!"false".equals(processSections)) { // NOI18N
            // Find and load manifest sections.
            Manifest mani = m.getManifest();
            if (mani == null) {
                throw new InvalidException(m, "no manifest");
            }
            for (Map.Entry<String,Attributes> entry : mani.getEntries().entrySet()) {
                ManifestSection section = ManifestSection.create(entry.getKey(), entry.getValue(), m);
                if (section != null) {
                    if (mysections == null) {
                        mysections = new HashSet<ManifestSection>(25);
                    }
                    mysections.add(section);
                }
            }
            if (mysections != null) {
                cache.findGlobalProperty("processSections", "false", "true"); // NOI18N
            }
        }
        String installClass = cache.findProperty(m, "OpenIDE-Module-Install", false); // NOI18N
        if (installClass != null) {
            String installClassName;
            try {
                installClassName = Util.createPackageName(installClass);
            } catch (IllegalArgumentException iae) {
                InvalidException ie = new InvalidException(m, iae.toString());
                ie.initCause(iae);
                throw ie;
            }
            if (installClass.endsWith(".ser")) throw new InvalidException(m, "Serialized module installs not supported: " + installClass); // NOI18N
            try {
                // We could simply load the ModuleInstall right away in all cases.
                // However consider a ModuleInstall that has a static or instance
                // initializer or initialize() calling NbBundle.getBundle(String). In the
                // old module installer that would work and it was never specifically discouraged,
                // so that needs to continue working. So do not resolve in most cases;
                // if the module specifically has a validate method, then this is clearly
                // documented to be called before systemClassLoader is ready, so the onus
                // is on the module author to avoid anything dangerous there.
                clazz = Class.forName(installClassName, false, m.getClassLoader());
                if (clazz.getClassLoader() != m.getClassLoader()) {
                    ev.log(Events.WRONG_CLASS_LOADER, m, clazz, m.getClassLoader());
                }
                // Search for a validate() method; if there is none, do not resolve the class now!
                Class<?> c;
                for (c = clazz; c != ModuleInstall.class && c != Object.class; c = c.getSuperclass()) {
                    try {
                        // #13997: do not search in superclasses.
                        c.getDeclaredMethod("validate"); // NOI18N
                        // It has one. We are permitted to resolve the class, create
                        // the installer instance, and validate it.
                        ModuleInstall install = SharedClassObject.findObject(clazz.asSubclass(ModuleInstall.class), true);
                        // The following can throw IllegalStateException, which we would
                        // rethrow as InvalidException below:
                        install.validate();
                    } catch (NoSuchMethodException nsme) {
                        // OK, did not find it here, continue.
                    }
                }
                if (c == Object.class) throw new ClassCastException("Should extend ModuleInstall: " + clazz.getName()); // NOI18N
                // Did not find any validate() method, so remember the class and resolve later.
            } catch (Exception t) {
                InvalidException ie = new InvalidException(m, t.toString());
                ie.initCause(t);
                throw ie;
            } catch (LinkageError t) {
                InvalidException ie = new InvalidException(m, t.toString());
                ie.initCause(t);
                throw ie;
            }
        }
        // For layer & help set, validate only that the base-locale resource
        // exists, not its contents or anything.
        String layerResource = cache.findProperty(m, "OpenIDE-Module-Layer", false); // NOI18N
        if (layerResource != null && !m.isNetigso()) {
            URL layer = m.getClassLoader().getResource(layerResource);
            if (layer == null) throw new InvalidException(m, "Layer not found: " + layerResource); // NOI18N
        }
        String helpSetName = cache.findProperty(m, "OpenIDE-Module-Description", false); // NOI18N
        if (helpSetName != null) {
            Util.err.log(Level.WARNING, "Use of OpenIDE-Module-Description in {0} is deprecated.", m.getCodeNameBase());
            Util.err.warning("(Please install help using an XML layer instead.)");
        }
        // We are OK, commit everything to our cache.
        if (mysections != null) {
            sections.put(m, mysections);
        }
        if (clazz != null) {
            installs.put(m, clazz.asSubclass(ModuleInstall.class));
        }
        if (layerResource != null) {
            layers.put(m, layerResource);
        }   
    }

    private void checkForHiddenPackages(Module m) throws InvalidException {
        List<Module.PackageExport> hiddenPackages = new ArrayList<Module.PackageExport>();
        List<Module> mWithDeps = new LinkedList<Module>();
        mWithDeps.add(m);
        if (mgr != null) {
            mWithDeps.addAll(mgr.getAttachedFragments(m));
            for (Dependency d : m.getDependencies()) {
                if (d.getType() == Dependency.TYPE_MODULE) {
                    Module _m = mgr.get((String) Util.parseCodeName(d.getName())[0]);
                    assert _m != null : d;
                    mWithDeps.add(_m);
                    mWithDeps.addAll(mgr.getAttachedFragments(_m));
                }
            }
        }
        for (Module _m : mWithDeps) {
            String hidden = cache.findProperty(_m, "OpenIDE-Module-Hide-Classpath-Packages", false); // NOI18N
            if (hidden != null) {
                for (String piece : hidden.trim().split("[ ,]+")) { // NOI18N
                    try {
                        if (piece.endsWith(".*")) { // NOI18N
                            String pkg = piece.substring(0, piece.length() - 2);
                            Dependency.create(Dependency.TYPE_MODULE, pkg);
                            if (pkg.lastIndexOf('/') != -1) {
                                throw new IllegalArgumentException("Illegal OpenIDE-Module-Hide-Classpath-Packages: " + hidden); // NOI18N
                            }
                            hiddenPackages.add(new Module.PackageExport(pkg.replace('.', '/') + '/', false));
                        } else if (piece.endsWith(".**")) { // NOI18N
                            String pkg = piece.substring(0, piece.length() - 3);
                            Dependency.create(Dependency.TYPE_MODULE, pkg);
                            if (pkg.lastIndexOf('/') != -1) {
                                throw new IllegalArgumentException("Illegal OpenIDE-Module-Hide-Classpath-Packages: " + hidden); // NOI18N
                            }
                            hiddenPackages.add(new Module.PackageExport(pkg.replace('.', '/') + '/', true));
                        } else {
                            throw new IllegalArgumentException("Illegal OpenIDE-Module-Hide-Classpath-Packages: " + hidden); // NOI18N
                        }
                    } catch (IllegalArgumentException x) {
                        throw new InvalidException(_m, x.getMessage());
                    }
                }
            }
        }
        if (!hiddenPackages.isEmpty()) {
            synchronized (hiddenClasspathPackages) {
                hiddenClasspathPackages.put(m, hiddenPackages);
                for (Module.PackageExport pkg : hiddenPackages) {
                    List<Module> ms = hiddenClasspathPackagesReverse.get(pkg);
                    if (ms == null) {
                        hiddenClasspathPackagesReverse.put(pkg, ms = new LinkedList<Module>());
                    }
                    ms.add(m);
                }
            }
        }
    }
    
    public void dispose(Module m) {
        Util.err.fine("dispose: " + m);
        // Events probably not needed here.
        Set<ManifestSection> s = sections.remove(m);
        if (s != null) {
            for (ManifestSection sect : s) {
                sect.dispose();
            }
        }
        installs.remove(m);
        layers.remove(m);
        kosherPackages.remove(m);
        synchronized (hiddenClasspathPackages) {
            hiddenClasspathPackages.remove(m);
            for (List<Module> ms : hiddenClasspathPackagesReverse.values()) {
                ms.remove(m);
                // could also delete entry if ms.isEmpty()
            }
        }
    }

    @Override
    protected void classLoaderUp(ClassLoader cl) {
        MainLookup.systemClassLoaderChanged(cl);
        ev.log(Events.PERF_TICK, "META-INF/services/ additions registered"); // NOI18N
    }

    final void waitOnStart() {
        onStartStop.waitOnStart();
    }
    
    @Override
    public void load(final List<Module> modules) {
        FileUtil.runAtomicAction(new Runnable() {
            @Override
            public void run() {
                loadImpl(modules);
            }
        });
    }
    
    private void loadImpl(List<Module> modules) {
        ev.log(Events.START_LOAD, modules);
        
        checkForDeprecations(modules);
        
        loadLayers(modules, true);
        ev.log(Events.PERF_TICK, "layers loaded"); // NOI18N
	
        onStartStop.initialize();
        ev.log(Events.PERF_TICK, "@OnStart"); // NOI18N

        ev.log(Events.PERF_START, "NbInstaller.load - sections"); // NOI18N
        ev.log(Events.LOAD_SECTION);
        CoreBridge.getDefault().loaderPoolTransaction(true);
        try {
            for (Module m: modules) {
                try {
                    loadSections(m, true);
                } catch (Exception t) {
                    Util.err.log(Level.SEVERE, null, t);
                } catch (LinkageError le) {
                    Util.err.log(Level.SEVERE, null, le);
                }
                ev.log(Events.PERF_TICK, "sections for " + m.getCodeName() + " loaded"); // NOI18N
            }
        } finally {
            CoreBridge.getDefault().loaderPoolTransaction(false);
        }
        ev.log(Events.PERF_END, "NbInstaller.load - sections"); // NOI18N

        // Yarda says to put this here.
        if (! initializedFolderLookup) {
            Util.err.fine("modulesClassPathInitialized");
            MainLookup.modulesClassPathInitialized();
            initializedFolderLookup = true;
        }
        
        // we need to initialize UI before we let modules run ModuleInstall.restore
        Main.initUICustomizations();

        ev.log(Events.PERF_START, "NbInstaller.load - ModuleInstalls"); // NOI18N
        for (Module m: modules) {
            try {
                loadCode(m, true);
            } catch (Exception t) {
                Util.err.log(Level.SEVERE, null, t);
            } catch (LinkageError le) {
                Util.err.log(Level.SEVERE, null, le);
            } catch (AssertionError e) {
                Util.err.log(Level.SEVERE, null, e);
            }
	    ev.log(Events.PERF_TICK, "ModuleInstall for " + m.getCodeName() + " called"); // NOI18N
        }
        ev.log(Events.PERF_END, "NbInstaller.load - ModuleInstalls"); // NOI18N

        ev.log(Events.FINISH_LOAD, modules);
        
        if (Boolean.getBoolean("netbeans.preresolve.classes")) {
            preresolveClasses(modules);
        }
    }
    
    final void preloadCache(Collection<Module> modules) {
        for (Module m : modules) {
            // initialize the cache
            isShowInAutoUpdateClient(m);
        }
    }
    
    @Override
    public void unload(final List<Module> modules) {
        FileUtil.runAtomicAction(new Runnable() {
            @Override
            public void run() {
                unloadImpl(modules);
            }
        });
    }
    
    private void unloadImpl(List<Module> modules) {
        ev.log(Events.START_UNLOAD, modules);
        for (Module m: modules) {
            try {
                loadCode(m, false);
            } catch (Exception t) {
                Util.err.log(Level.SEVERE, null, t);
            } catch (LinkageError le) {
                Util.err.log(Level.SEVERE, null, le);
            }
        }
        CoreBridge.getDefault().loaderPoolTransaction(true);
        try {
            for (Module m: modules) {
                try {
                    loadSections(m, false);
                } catch (Exception t) {
                    Util.err.log(Level.SEVERE, null, t);
                } catch (LinkageError le) {
                    Util.err.log(Level.SEVERE, null, le);
                }
            }
        } finally {
            try {
                CoreBridge.getDefault().loaderPoolTransaction(false);
            } catch (RuntimeException e) {
                Util.err.log(Level.SEVERE, null, e);
            }
        }
        loadLayers(modules, false);
        ev.log(Events.FINISH_UNLOAD, modules);
    }
    
    /** Load/unload installer code for a module. */
    @SuppressWarnings("deprecation") // old ModuleInstall methods we have to call
    private void loadCode(Module m, boolean load) throws Exception {
        Class<? extends ModuleInstall> instClazz = installs.get(m);
        if (instClazz != null) {
            ModuleInstall inst = SharedClassObject.findObject(instClazz, true);
            if (load) {
                ev.log(Events.RESTORE, m);
                inst.restored();
            } else {
                ev.log(Events.UNINSTALL, m);
                inst.uninstalled();
            }
        }
    }
    
    /** Load/unload all manifest sections for a given module. */
    @SuppressWarnings("deprecation") // old ManifestSection.* we have to interpret
    private void loadSections(Module m, boolean load) throws Exception {
        Set<ManifestSection> s = sections.get(m);
        if (s == null) {
            return;
        }
        // Whether we yet had occasion to attach to the module actions list.
        boolean attachedToMA = false;
        try {
	    ev.log(Events.LOAD_SECTION);
            for (ManifestSection sect : s) {
                if (sect instanceof ManifestSection.ActionSection) {
                    if (! attachedToMA) {
                        // First categorize the actions we will add.
                        Object category = m.getLocalizedAttribute("OpenIDE-Module-Display-Category"); // NOI18N
                        if (category == null) {
                            // uncategorized modules group by themselves
                            category = m.getCodeNameBase();
                        }
                        CoreBridge.getDefault().attachToCategory(category);
                        attachedToMA = true;
                    }
                    CoreBridge.getDefault ().loadActionSection((ManifestSection.ActionSection)sect, load);
                } else if (sect instanceof ManifestSection.ClipboardConvertorSection) {
                    loadGenericSection(sect, load);
                } else if (sect instanceof ManifestSection.DebuggerSection) {
                    loadGenericSection(sect, load);
                } else if (sect instanceof ManifestSection.LoaderSection) {
                    CoreBridge.getDefault().loadLoaderSection((ManifestSection.LoaderSection)sect, load);
                } else {
                    assert false : sect;
                }
            }
        } finally {
            if (attachedToMA) {
                CoreBridge.getDefault ().attachToCategory (null);
            }
        }
    }
    
    // Load or unload various possible manifest sections.
    
    /** Simple section that can just be passed to lookup.
     * The lookup sees the real object, not the section.
     * You tell it whether to convert the result to the real
     * instance, or just register the section itself.
     */
    private void loadGenericSection(ManifestSection s, boolean load) {
        CoreBridge.getDefault().loadDefaultSection(s, convertor, load);
    }
    
    private final InstanceContent.Convertor<ManifestSection,Object> convertor = new Convertor();
    private final class Convertor implements InstanceContent.Convertor<ManifestSection,Object> { // or <ManifestSection,SharedClassObject>?
        Convertor() {}
        public Object convert(ManifestSection s) {
            try {
                return s.getInstance();
            } catch (Exception e) {
                Util.err.log(Level.WARNING, null, e);
                // Try to remove it from the pool so it does not continue
                // to throw errors over and over. Hopefully it is kosher to
                // do this while it is in the process of converting! I.e.
                // hopefully InstanceLookup is well-synchronized.
                loadGenericSection(s, false);
                return null;
            }
        }
        public Class<?> type(ManifestSection s) {
            return s.getSuperclass();
        }
        
        /** Computes the ID of the resulted object.
         * @param obj the registered object
         * @return the ID for the object
         */
        public String id(ManifestSection obj) {
            return obj.toString ();
        }
        
        /** The human presentable name for the object.
         * @param obj the registered object
         * @return the name representing the object for the user
         */
        public String displayName(ManifestSection obj) {
            return obj.toString ();
        }
        
    }
    
    /** Either load or unload the layer, if any, for a set of modules.
     * If the parameter load is true, load it, else unload it.
     * Locale/branding variants are likewise loaded or unloaded.
     * If a module has no declared layer, does nothing.
     */
    void loadLayers(List<Module> modules, boolean load) {
        ev.log(load ? Events.LOAD_LAYERS : Events.UNLOAD_LAYERS, modules);
        // #23609: dependent modules should be able to override:
        modules = new ArrayList<Module>(modules);
        Collections.reverse(modules);
        Map<ModuleLayeredFileSystem,Collection<URL>> urls = new HashMap<ModuleLayeredFileSystem,Collection<URL>>(5);
        ModuleLayeredFileSystem userModuleLayer = ModuleLayeredFileSystem.getUserModuleLayer();
        ModuleLayeredFileSystem installationModuleLayer = ModuleLayeredFileSystem.getInstallationModuleLayer();
        urls.put(userModuleLayer, new LinkedHashSet<URL>(1000));
        urls.put(installationModuleLayer, new LinkedHashSet<URL>(1000));
        for (Module m: modules) {
            // #19458: only put reloadables into the "session layer"
            // (where they will not have their layers cached). All others
            // should go into "installation layer" (so that they can mask
            // layers according to cross-dependencies).
            ModuleLayeredFileSystem host = m.isReloadable() ? userModuleLayer : installationModuleLayer;
            Collection<URL> theseurls = urls.get(host);
            if (theseurls == null) {
                theseurls = new LinkedHashSet<URL>(1000);
                urls.put(host, theseurls);
            }
            String s = layers.get(m);
            if (s != null) {
                Util.err.log(Level.FINE, "loadLayer: {0} load={1}", new Object[] { s, load });
                // Actually add a sequence of layers, in locale order.
                String base, ext;
                int idx = s.lastIndexOf('.'); // NOI18N
                if (idx == -1) {
                    base = s;
                    ext = ""; // NOI18N
                } else {
                    base = s.substring(0, idx);
                    ext = s.substring(idx);
                }
                boolean foundSomething = false;
                for (String suffix : NbCollections.iterable(NbBundle.getLocalizingSuffixes())) {
                    String resource = base + suffix + ext;
                    Enumeration<URL> en = m.findResources(resource);
                    if (en.hasMoreElements()) {
                        URL u = en.nextElement();
                        theseurls.add(u);
                        foundSomething = true;
                        if (en.hasMoreElements()) {
                            String patchesClassPath = System.getProperty("netbeans.patches." + m.getCodeNameBase()); // NOI18N
                            assert patchesClassPath != null : "At most one resource per module: " + m; // NOI18N
                            Util.err.log(Level.INFO, "Using {0} as layer for {1} not {2}", new Object[]{u, m.getCodeNameBase(), en.nextElement()}); // NOI18N
                        }
                    }
                }
                if (! foundSomething) {
                    // Should never happen (we already checked in prepare() for base layer)...
                    Util.err.fine("Module layer not found: " + s);
                    continue;
                }
            }
            Enumeration e = m.findResources("META-INF/generated-layer.xml"); // NOI18N
            while (e.hasMoreElements()) {
                URL u = (URL)e.nextElement();
                theseurls.add(u);
            }
        }
        // Now actually do it.
        for (Map.Entry<ModuleLayeredFileSystem,Collection<URL>> entry: urls.entrySet()) {
            ModuleLayeredFileSystem host = entry.getKey();
            Collection<URL> theseurls = entry.getValue();
            Util.err.log(Level.FINE, "Adding/removing layer URLs: host={0} urls={1}", new Object[] { host, theseurls });
            try {
                if (load) {
                    host.addURLs(theseurls);
                } else {
                    // #106737: we might have the wrong host, since it switches when reloadable flag is toggled.
                    // To be safe, remove from both.
                    userModuleLayer.removeURLs(theseurls);
                    installationModuleLayer.removeURLs(theseurls);
                }
            } catch (Exception e) {
                Util.err.log(Level.WARNING, null, e);
            }
        }
    }
    
    /** Scan a (nondeprecated) module for direct dependencies on deprecated modules.
     * Deprecated modules can quietly depend on other deprecated modules.
     * And if the module is not actually enabled, it does not matter.
     * Indirect dependencies are someone else's problem.
     * Provide-require dependencies do not count either.
     * @param modules the modules which are now being turned on
     */
    private void checkForDeprecations(List<Module> modules) {
        Map<String,Set<String>> depToUsers = new TreeMap<String,Set<String>>();
        for (Module m : modules) {
            String depr = cache.findProperty(m, "OpenIDE-Module-Deprecated", false); // NOI18N
            if (!Boolean.parseBoolean(depr)) { 
                for (Dependency dep : m.getDependencies()) {
                    if (dep.getType() == Dependency.TYPE_MODULE) {
                        String cnb = (String) Util.parseCodeName(dep.getName())[0];
                        Set<String> users = depToUsers.get(cnb);
                        if (users == null) {
                            users = new TreeSet<String>();
                            depToUsers.put(cnb, users);
                        }
                        users.add(m.getCodeNameBase());
                    }
                }
            }
        }
        for (Map.Entry<String,Set<String>> entry : depToUsers.entrySet()) {
            String dep = entry.getKey();
            Module o = mgr.get(dep);
            assert o != null : "No such module: " + dep;
            String depr = cache.findProperty(o, "OpenIDE-Module-Deprecated", false); // NOI18N
            if (Boolean.parseBoolean(depr)) {
                String message = cache.findProperty(o, "OpenIDE-Module-Deprecation-Message", true); // NOI18N
                // XXX use NbEvents? I18N?
                // For now, assume this is a developer-oriented message that need not be localized or displayed in a pretty fashion.
                Set<String> users = entry.getValue();
                if (message != null) {
                    Util.err.log(Level.WARNING, "the modules {0} use {1} which is deprecated: {2}", new Object[] {users, dep, message});
                } else {
                    Util.err.log(Level.WARNING, "the modules {0} use {1} which is deprecated.", new Object[] {users, dep});
                }
            }
        }
    }
        
    public boolean closing(List<Module> modules) {
        Util.err.fine("closing: " + modules);
        for (Module m: modules) {
            Class<? extends ModuleInstall> instClazz = installs.get(m);
            if (instClazz != null) {
                try {
                    ModuleInstall inst = SharedClassObject.findObject(instClazz, true);
                    if (! inst.closing()) {
                        Util.err.fine("Module " + m + " refused to close");
                        return false;
                    }
                } catch (RuntimeException re) {
                    Util.err.log(Level.SEVERE, null, re);
                    // continue, assume it is trash
                } catch (LinkageError le) {
                    Util.err.log(Level.SEVERE, null, le);
                }
            }
        }
        return onStartStop.closing(modules);
    }
    
    @Override
    public void close(List<Module> modules) {
        closeAsync(modules).waitFinished();
    }

    @Override
    public Task closeAsync(List<Module> modules) {
        Util.err.fine("close: " + modules);
        ev.log(Events.CLOSE);
        moduleList.shutDown();
        List<Task> waitFor = onStartStop.startClose(modules);
        // [PENDING] this may need to write out changed ModuleInstall externalized
        // forms...is that really necessary to do here, or isn't it enough to
        // do right after loading etc.? Currently these are only written when
        // a ModuleInstall has just been restored etc. which is probably fine.
        for (Module m : modules) {
            Class<? extends ModuleInstall> instClazz = installs.get(m);
            if (instClazz != null) {
                try {
                    ModuleInstall inst = SharedClassObject.findObject(instClazz, true);
                    if (inst == null) throw new IllegalStateException("Inconsistent state: " + instClazz); // NOI18N
                    inst.close();
                } catch (ThreadDeath td) {
                    throw td;
                } catch (Throwable t) {
                    // Catch even the heavy stuff here, we are going away.
                    Util.err.log(Level.SEVERE, null, t);
                    // oh well
                }
            }
        }
        waitFor.add(WarmUpSupport.waitTask());
        return new ProxyTask(waitFor);
    }
    private static String cacheCnb;
    private static Set<Dependency> cacheDeps;
    @Override
    protected Set<Dependency> loadDependencies(String cnb) {
        return cnb.equals(cacheCnb) ? cacheDeps : null;
    }
    @SuppressWarnings("unchecked")
    static void register(String name, Object obj) {
        cacheCnb = name;
        cacheDeps = (Set<Dependency>)obj;
    }

    
    /** Overridden to perform automatic API upgrades.
     * That is, should do nothing on new modules, but for older ones will
     * automatically make them depend on things they need.
     * This is now all handled from declarative configuration files:
     * in the system filesystem, ModuleAutoDeps/*.xml may be added
     * according to the DTD "-//NetBeans//DTD Module Automatic Dependencies 1.0//EN".
     */
    public @Override void refineDependencies(Module m, Set<Dependency> dependencies) {
        /* JST-PENDING just tring to comment this out
        // All modules implicitly depend on the APIs somehow.
        if (!m.getCodeNameBase().equals("org.openide") &&
                Util.getModuleDep(dependencies, "org.openide") == null) {
            dependencies.addAll(Dependency.create(Dependency.TYPE_MODULE, "org.openide/1 > 0")); // NOI18N
        }
         */
        if (Boolean.getBoolean("org.netbeans.core.modules.NbInstaller.noAutoDeps")) {
            // Skip them all - useful for unit tests.
            return;
        }
        AutomaticDependencies.Report rep = AutomaticDependencies.getDefault().refineDependenciesAndReport(m.getCodeNameBase(), dependencies);
        if (rep.isModified()) {
            Util.err.warning(rep.toString());
        }
    }

    public @Override String[] refineProvides (Module m) {
        if (m.getCodeNameBase ().equals ("org.openide.modules")) { // NOI18N
            List<String> arr = new ArrayList<String>(4);
            CoreBridge.defineOsTokens(arr);
            
            // module format is now 2
            arr.add("org.openide.modules.ModuleFormat1"); // NOI18N
            arr.add("org.openide.modules.ModuleFormat2"); // NOI18N
            
            return arr.toArray (new String[arr.size()]);
        }
        return null;
    }
    
    public @Override boolean shouldDelegateResource(Module m, Module parent, String pkg) {
        //Util.err.fine("sDR: m=" + m + " parent=" + parent + " pkg=" + pkg);
        // Cf. #19622:
        if (parent == null) {
            // Application classpath checks.
            for (String cppkg : CLASSPATH_PACKAGES) {
                if (pkg.startsWith(cppkg) && !findKosher(m).contains(cppkg)) {
                    // Undeclared use of a classpath package. Refuse it.
                    if (LOG.isLoggable(Level.FINE)) {
                        LOG.fine("Refusing to load classpath package " + pkg + " for " + m.getCodeNameBase() + " without a proper dependency"); // NOI18N
                    }
                    return false;
                }
            }
            List<Module.PackageExport> hiddenPackages;
            synchronized (hiddenClasspathPackages) {
                hiddenPackages = hiddenClasspathPackages.get(m);
            }
            if (hiddenPackages != null) {
                for (Module.PackageExport hidden : hiddenPackages) {
                    if (hidden.recursive ? pkg.startsWith(hidden.pkg) : pkg.equals(hidden.pkg)) {
                        if (LOG.isLoggable(Level.FINE)) {
                            LOG.fine("Refusing to load classpath package " + pkg + " for " + m.getCodeNameBase());
                        }
                        return false;
                    }
                }
            }
            if (!checkBootDelegation(pkg)) {
                return false;
            }
        }
        if (LOG.isLoggable(Level.FINER) && /* otherwise ClassCircularityError on LogRecord*/!pkg.equals("java/util/logging/")) {
            LOG.finer("Delegating resource " + pkg + " from " + parent + " for " + m.getCodeNameBase());
        }
        return true;
    }

    public @Override boolean shouldDelegateClasspathResource(String pkg) {
        synchronized (hiddenClasspathPackages) {
            for (Map.Entry<Module.PackageExport,List<Module>> entry : hiddenClasspathPackagesReverse.entrySet()) {
                Module.PackageExport hidden = entry.getKey();
                if (hidden.recursive ? pkg.startsWith(hidden.pkg) : pkg.equals(hidden.pkg)) {
                    for (Module m : entry.getValue()) {
                        if (m.isEnabled()) {
                            if (LOG.isLoggable(Level.FINE)) {
                                LOG.fine("Refusing to load classpath package " + pkg + " because of " + m.getCodeNameBase());
                            }
                            return false;
                        }
                    }
                }
            }
        }
        return checkBootDelegation(pkg);
    }
    
    private boolean checkBootDelegation(String pkg) {
        String del = System.getProperty("netbeans.bootdelegation"); // NOI18N
        if (del != null) {
            if (!pkg.startsWith("java/")) {
                boolean allowed = false;
                for (String s : del.split(",")) {
                    s = s.trim();
                    if (s.endsWith(".*")) {
                        s = s.substring(0, s.length() - 2).replace('.', '/') + '/';
                        if (pkg.startsWith(s) && pkg.length() > s.length()) {
                            allowed = true;
                            break;
                        }
                    } else {
                        s = s.replace('.', '/') + '/';
                        if (pkg.equals(s)) {
                            allowed = true;
                            break;
                        }
                    }
                }
                if (!allowed) {
                    return false;
                }
            }
        }
        return true;
    }
    
    private static final String[] CLASSPATH_PACKAGES = {
        // core.jar shall be inaccessible
        "org/netbeans/core/startup/",
        // Do not add JRE packages here! See issue #96711 for the alternative.
    };
    
    private Set<String> findKosher(Module m) {
        Set<String> s = kosherPackages.get(m);
        if (s == null) {
            s = new HashSet<String>();
            Set<Dependency> deps = m.getDependencies();
            SpecificationVersion openide = Util.getModuleDep(deps, "org.openide"); // NOI18N
            boolean pre27853 = (openide == null || openide.compareTo(new SpecificationVersion("1.3.12")) < 0); // NOI18N
            for (Dependency dep : deps) {
                // Extend this for other classpath modules:
                if (dep.getType() == Dependency.TYPE_MODULE &&
                        dep.getName().equals("org.netbeans.core.startup/1")) { // NOI18N
                    // Various modules (incl. o.n.core) dep on this as friends and need to access it.
                    s.add("org/netbeans/core/startup/"); // NOI18N
                } else if (pre27853 && dep.getType() == Dependency.TYPE_MODULE) {
                    // Module dependency. If a package was kosher for A and B depends
                    // on A, we let B use it undeclared. Cf. javacvs -> vcscore & RE.
                    // But #27853: only do this for old modules.
                    String name = dep.getName();
                    int idx = name.indexOf('/');
                    if (idx != -1) {
                        name = name.substring(0, idx);
                    }
                    Module other = mgr.get(name);
                    if (other == null) throw new IllegalStateException("Should have found dep " + dep + " from " + m); // NOI18N
                    s.addAll(findKosher(other));
                } else if (dep.getType() == Dependency.TYPE_PACKAGE) {
                    String depname = dep.getName();
                    String req;
                    int idx = depname.indexOf('['); // NOI18N
                    if (idx == -1) {
                        // depname = org.apache.xerces.parsers
                        // req = org/apache/xerces/parsers/
                        req = depname.replace('.', '/').concat("/"); // NOI18N
                    } else if (idx == 0) {
                        // depname = [org.apache.xerces.parsers.DOMParser]
                        // req = org/apache/xerces/parsers/
                        int idx2 = depname.lastIndexOf('.');
                        req = depname.substring(1, idx2).replace('.', '/').concat("/"); // NOI18N
                    } else {
                        // depname = org.apache.xerces.parsers[DOMParser]
                        // req = org/apache/xerces/parsers/
                        req = depname.substring(0, idx).replace('.', '/').concat("/"); // NOI18N
                    }
                    for (String cppkg : CLASSPATH_PACKAGES) {
                        if (req.startsWith(cppkg)) {
                            // Module requested this exact package or some subpackage or
                            // a class in one of these packages; it is kosher.
                            s.add(cppkg);
                        }
                    }
                }
            }
            if (s.isEmpty()) s = Collections.<String>emptySet();
            kosherPackages.put(m, s);
        }
        return s;
    }
    
    /** Information about contents of some JARs on the startup classpath (both lib/ and lib/ext/).
     * The first item in each is a prefix for JAR filenames.
     * For a JAR matching such a prefix, the remaining items are entries
     * from CLASSPATH_PACKAGES.
     * <p>In this case, the JAR is only on a module's effective classpath in case
     * {@link #findKosher} reports that at least one of the packages is requested -
     * in which case that package and any descendants, but not other packages in the JAR,
     * are included in the effective classpath.
     * <p>JARs which are not listed here but are in lib/ or lib/ext/ are assumed to be
     * in the effective classpath of every module, automatically.
     * <p>Note that updater.jar is *not* on the startup classpath and is explicitly
     * added to autoupdate.jar via Class-Path, so it need not concern us here -
     * it should appear on the effective classpath of autoupdate only.
     * @see "#22466"
     */
    private static final String[][] CLASSPATH_JARS = {
        {"core", "org/netbeans/core/", "org/netbeans/beaninfo/"}, // NOI18N
        // No one ought to be using boot.jar:
        {"boot"}, // NOI18N
    };
    
    /** Get the effective "classpath" used by a module.
     * Specific syntax: classpath entries as usual, but
     * they may be qualified with packages, e.g.:
     * <code>/path/to/api-module.jar[org.netbeans.api.foo.*,org.netbeans.spi.foo.**]:/path/to/wide-open.jar</code>
     * @see ModuleSystem#getEffectiveClasspath
     * @see "#22466"
     */
    String getEffectiveClasspath(Module m) {
        if (!m.isEnabled()) {
            // For disabled modules, we do not know for sure what it can load.
            return ""; // NOI18N
        }
        // The classpath entries - each is a filename possibly followed by package qualifications.
        List<String> l = new ArrayList<String>(100);
        // Start with boot classpath.
        createBootClassPath(l);
        // Move on to "startup classpath", qualified by applicable package deps etc.
        // Fixed classpath modules don't get restricted in this way.
        Set<String> kosher = m.isFixed() ? null : findKosher(m);
        StringTokenizer tok = new StringTokenizer(System.getProperty("java.class.path", ""), File.pathSeparator);
        while (tok.hasMoreTokens()) {
            addStartupClasspathEntry(new File(tok.nextToken()), l, kosher);
        }
        // See org.netbeans.Main for actual computation of the dynamic classpath.
        tok = new StringTokenizer(System.getProperty("netbeans.dynamic.classpath", ""), File.pathSeparator);
        while (tok.hasMoreTokens()) {
            addStartupClasspathEntry(new File(tok.nextToken()), l, kosher);
        }
        // Finally include this module and its dependencies recursively.
        // Modules whose direct classpath has already been added to the list:
        Set<Module> modulesConsidered = new HashSet<Module>(50);
        // Code names of modules on which this module has an impl dependency
        // (so can use any package):
        Set<String> implDeps = new HashSet<String>(10);
        for (Dependency dep : m.getDependencies()) {
            // Remember, provide-require deps do not affect classpath!
            if (dep.getType() == Dependency.TYPE_MODULE && dep.getComparison() == Dependency.COMPARE_IMPL) {
                // We can assume the impl dep has the correct version;
                // otherwise this module could not have been enabled to begin with.
                implDeps.add(dep.getName());
            }
        }
        SpecificationVersion openide = Util.getModuleDep(m.getDependencies(), "org.openide"); // NOI18N
        boolean pre27853 = (openide == null || openide.compareTo(new SpecificationVersion("1.3.12")) < 0); // NOI18N
        // #27853: only make recursive for old modules.
        addModuleClasspathEntries(m, m, modulesConsidered, implDeps, l, pre27853 ? Integer.MAX_VALUE : 1);
        // Done, package it all up as a string.
        StringBuilder buf = new StringBuilder(l.size() * 100 + 1);
        for (String s: l) {
            if (buf.length() > 0) {
                buf.append(File.pathSeparatorChar);
            }
            buf.append(s);
        }
        return buf.toString();
    }
    
    // Copied from NbClassPath:
    private static void createBootClassPath(List<String> l) {
        // boot
        String boot = System.getProperty("sun.boot.class.path"); // NOI18N
        if (boot != null) {
            StringTokenizer tok = new StringTokenizer(boot, File.pathSeparator);
            while (tok.hasMoreTokens()) {
                l.add(tok.nextToken());
            }
        }
        
        // std extensions
        String extensions = System.getProperty("java.ext.dirs"); // NOI18N
        if (extensions != null) {
            for (StringTokenizer st = new StringTokenizer(extensions, File.pathSeparator); st.hasMoreTokens();) {
                File dir = new File(st.nextToken());
                File[] entries = dir.listFiles();
                if (entries != null) {
                    for (File f : entries) {
                        String name = f.getName().toLowerCase(Locale.US);
                        if (name.endsWith(".zip") || name.endsWith(".jar")) { // NOI18N
                            l.add(f.getAbsolutePath());
                        }
                    }
                }
            }
        }
    }
    
    /** Add a classpath entry from the lib/ or lib/ext/ dirs, if appropriate.
     * @param entry a classpath entry; either a directory, or a JAR file which might be controlled
     * @param cp the classpath (<code>List&lt;String&gt;</code>) to add to
     * @param kosher known packages which may be accessed (<code>Set&lt;String&gt;</code>), or null for no restrictions
     * @see "#22466"
     */
    private static void addStartupClasspathEntry(File cpEntry, List<String> cp, Set<String> kosher) {
        if (cpEntry.isDirectory()) {
            cp.add(cpEntry.getAbsolutePath());
            return;
        }
        // JAR or ZIP. Check whether we can access it.
        String name = cpEntry.getName();
        for (String[] cpjar : CLASSPATH_JARS) {
            if (kosher != null && name.startsWith(cpjar[0])) {
                // Restricted JAR.
                StringBuilder entry = null; // will be set if there are any packages
                for (int k = 1; k < cpjar.length; k++) {
                    String pkg = cpjar[k];
                    if (kosher.contains(pkg)) {
                        // Module is permitted to use this package.
                        if (entry == null) {
                            entry = new StringBuilder(100);
                            entry.append(cpEntry.getAbsolutePath());
                            entry.append('['); // NOI18N
                        } else {
                            entry.append(','); // NOI18N
                        }
                        // pkg format: org/foo/bar/
                        entry.append(pkg.replace('/', '.')); // NOI18N
                        entry.append("**"); // NOI18N
                    }
                }
                if (entry != null) {
                    // Had >= 1 packages available from this JAR.
                    entry.append(']'); // NOI18N
                    cp.add(entry.toString());
                }
                return;
            }
        }
        // Did not match any, assumed to be on classpath.
        cp.add(cpEntry.getAbsolutePath());
    }

    /** Recursively build a classpath based on the module dependency tree.
     * @param m the current module whose JAR(s) might be added to the classpath
     * @param orig the module whose classpath we are ultimately trying to compute
     * @param considered modules we have already handled
     * @param implDeps module code names on which the orig module has an impl dep
     * @param cp the classpath to add to
     * @param depth the recursion depth to go to: 0 = only m's JAR, 1 = m + its parents, ..., MAX_INT = full recursion
     * @see "#22466"
     */
    private void addModuleClasspathEntries(Module m, Module orig, Set<Module> considered, Set<String> implDeps, List<String> cp, int depth) {
        // Head recursion so that baser modules are added to the front of the classpath:
        if (!considered.add(m)) return;
        for (Dependency dep : m.getDependencies()) {
            if (dep.getType() == Dependency.TYPE_MODULE) {
                String cnb = (String) Util.parseCodeName(dep.getName())[0];
                Module next = mgr.get(cnb);
                if (next == null) throw new IllegalStateException("No such module: " + cnb); // NOI18N
                if (depth > 0) {
                    addModuleClasspathEntries(next, orig, considered, implDeps, cp, depth - 1);
                }
            }
        }
        // Now add entries from m, if applicable.
        // We are friendly if we are considering the original module itself,
        // or if the original module has a (direct) impl dep on this module.
        boolean friend = (m == orig) || implDeps.contains(m.getCodeName());
        // Friend modules export everything to the classpath.
        // Otherwise, there may or may not be package restrictions.
        Module.PackageExport[] exports = friend ? null : m.getPublicPackages();
        String qualification = ""; // NOI18N
        if (exports != null) {
            if (exports.length == 0) {
                // Everything is blocked.
                return;
            }
            // Only certain packages are exported, so list them explicitly.
            StringBuilder b = new StringBuilder(100);
            b.append('['); // NOI18N
            for (int i = 0; i < exports.length; i++) {
                if (i > 0) {
                    b.append(','); // NOI18N
                }
                b.append(exports[i].pkg.replace('/', '.')); // NOI18N
                b.append(exports[i].recursive ? "**" : "*"); // NOI18N
            }
            b.append(']'); // NOI18N
            qualification = b.toString();
        }
        for (File jar : m.getAllJars()) {
            cp.add(jar.getAbsolutePath() + qualification);
        }
    }
    
    /** Check all module classes to make sure there are no unresolvable compile-time
     * dependencies. Turn on this mode with
     * <code>-J-Dnetbeans.preresolve.classes=true</code>
     * May be more useful to run org.netbeans.core.ValidateClassLinkageTest.
     * @param modules a list of modules, newly enabled, to check; fixed modules will be ignored
     */
    private void preresolveClasses(List<Module> modules) {
        Util.err.info("Pre-resolving classes for all loaded modules...be sure you have not specified -J-Xverify:none in ide.cfg");
        for (Module m : modules) {
            if (m.isFixed()) continue;
            if (m.getJarFile() == null) continue;
            File jar = m.getJarFile();
            // Note: extension JARs not checked.
            try {
                JarFile j = new JarFile(jar, true);
                try {
                    for (JarEntry entry : NbCollections.iterable(j.entries())) {
                        String name = entry.getName();
                        if (name.endsWith(".class")) { // NOI18N
                            String clazz = name.substring(0, name.length() - 6).replace('/', '.'); // NOI18N
                            Throwable t = null;
                            try {
                                Class.forName(clazz, false, m.getClassLoader());
                            } catch (ClassNotFoundException cnfe) { // e.g. "Will not load classes from default package" from ProxyClassLoader
                                t = cnfe;
                            } catch (LinkageError le) {
                                t = le;
                            } catch (RuntimeException re) { // e.g. IllegalArgumentException from package defs
                                t = re;
                            }
                            if (t != null) {
                                // XXX #106153: consider excluding mobility/ant-ext classes
                                Util.err.log(Level.WARNING, "From " + clazz + " in " + m.getCodeNameBase() + " with effective classpath " + getEffectiveClasspath(m), t);
                            }
                        }
                    }
                } finally {
                    j.close();
                }
            } catch (IOException ioe) {
                Util.err.log(Level.WARNING, null, ioe);
            }
        }
    }
    
    final boolean isShowInAutoUpdateClient(ModuleInfo m) {
        String show = cache.findProperty(m, "AutoUpdate-Show-In-Client", false); // NOI18N
        if (show != null) {
            return Boolean.parseBoolean(show);
        }
        // OSGi bundles should be considered invisible by default since they are typically autoloads.
        // (NB modules get AutoUpdate-Show-In-Client inserted into the JAR by the build process.)
        if (m instanceof Module) {
            return !((Module)m).isNetigso();
        }
        return true;
    }
    
    /** Cache important attributes from module manifests */
    static class Cache implements Stamps.Updater {
        private static final String CACHE = "all-installer.dat"; // NOI18N
        private final boolean modulePropertiesCached;
        private final Properties moduleProperties;

        public Cache() {
            InputStream is = Stamps.getModulesJARs().asStream(CACHE);
            IF:
            if (is != null) {
                Properties p = new Properties();
                try {
                    p.load(is);
                    is.close();
                } catch (IOException ex) {
                    LOG.log(Level.INFO, "Can't load all-installer.dat", ex);
                    break IF;
                }
                moduleProperties = p;
                modulePropertiesCached = true;
                return;
            }
            moduleProperties = new Properties();
            modulePropertiesCached = false;
        }

        final String findProperty(ModuleInfo m, String name, boolean localized) {
            final String fullName = m.getCodeNameBase() + '.' + name;
            final String nullValue = "\u0000"; // NOI18N
            if (modulePropertiesCached) {
                String val = moduleProperties.getProperty(fullName);
                if (nullValue.equals(val)) { 
                    return null;
                }
                if (val != null) {
                    return val;
                }
                LOG.log(Level.FINE, "not cached value: {0} for {1}", new Object[]{name, m});
            } 
            Object p = localized ? m.getLocalizedAttribute(name) : m.getAttribute(name);
            if (p == null) {
                moduleProperties.setProperty(fullName, nullValue);
                Stamps.getModulesJARs().scheduleSave(this, CACHE, false);
                return null;
            }
            String prop = p instanceof String ? (String)p : null;
            if (prop != null) {
                moduleProperties.setProperty(fullName, prop);
                Stamps.getModulesJARs().scheduleSave(this, CACHE, false);
            }
            return prop;
        }

        final String findGlobalProperty(String name, String expValue, String replaceValue) {
            assert name != null;
            assert replaceValue != null;
            if (modulePropertiesCached) {
                return moduleProperties.getProperty(name);
            } else {
                final Object prevValue = moduleProperties.get(name);
                if (BaseUtilities.compareObjects(expValue, prevValue)) {
                    moduleProperties.put(name, replaceValue);
                }
                Stamps.getModulesJARs().scheduleSave(this, CACHE, false);
                return null;
            }
        }

        @Override
        public void flushCaches(DataOutputStream os) throws IOException {
            moduleProperties.store(os, null);
        }

        @Override
        public void cacheReady() {
        }
    } // end of Cache
}
